├── .DS_Store
├── .gitattributes
├── LICENSE
├── README.md
├── WWDC
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Package.resolved
├── Package.swift
├── Sources
│ ├── WWDC
│ │ └── WWDC.swift
│ ├── WWDCClient
│ │ └── main.swift
│ └── WWDCMacros
│ │ └── WWDCMacro.swift
└── Tests
│ └── WWDCTests
│ └── WWDCTests.swift
├── cover.jpg
├── discover-calendar-and-eventkit
├── .DS_Store
├── README.md
├── discover-calendar-and-eventkit.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── huangrunhua.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── huangrunhua.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── discover-calendar-and-eventkit
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ └── movie.imageset
│ │ │ ├── Contents.json
│ │ │ └── p2891257290.jpg
│ ├── ContentView.swift
│ ├── EventEditView.swift
│ ├── Info.plist
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Ticket.swift
│ └── discover_calendar_and_eventkitApp.swift
└── images
│ └── IMG_7703.JPEG
├── discover-observation-in-swiftui
├── .DS_Store
├── README.md
├── discover-observation-in-swiftui.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── huangrunhua.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── huangrunhua.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── discover-observation-in-swiftui
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── OO-version-example
│ │ ├── ContentView_OOVersion.swift
│ │ └── ModelData_OOversion.swift
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── bindable-example
│ │ ├── Article.swift
│ │ └── ArticleEditView.swift
│ ├── discover_observation_in_swiftuiApp.swift
│ ├── environment-example
│ │ ├── Account.swift
│ │ └── AccountView.swift
│ ├── observable-in-array-example
│ │ ├── Book.swift
│ │ └── BooksList.swift
│ └── state-example
│ │ ├── ContentView.swift
│ │ ├── ModelData.swift
│ │ └── Music.swift
└── pic1.jpg
├── lift-subjects-from-images
├── .DS_Store
├── README.md
├── cover.png
├── lift-subjects-from-images.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── huangrunhua.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── huangrunhua.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── lift-subjects-from-images
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── cat.imageset
│ │ ├── Contents.json
│ │ └── FqdQZsHWAAA5AzC.jpg
│ └── dog.imageset
│ │ ├── Contents.json
│ │ └── dog.png
│ ├── ContentView.swift
│ ├── ImageLift.swift
│ ├── ImageLiftView.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── lift_subjects_from_imagesApp.swift
├── meet-storekit-for-swiftui
├── .DS_Store
├── README.md
├── article-images
│ ├── .DS_Store
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── IMG_7754.JPEG
│ └── IMG_7762.JPEG
├── meet-storekit-for-swiftui.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── huangrunhua.xcuserdatad
│ │ │ ├── IDEFindNavigatorScopes.plist
│ │ │ └── UserInterfaceState.xcuserstate
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── meet-storekit-for-swiftui.xcscheme
│ └── xcuserdata
│ │ └── huangrunhua.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── meet-storekit-for-swiftui
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ └── Musics
│ │ ├── Cold Winter.imageset
│ │ ├── Contents.json
│ │ └── IMG_7731.jpg
│ │ ├── Contents.json
│ │ ├── Drunk.imageset
│ │ ├── Contents.json
│ │ └── IMG_7729.jpg
│ │ └── Platinum Disco.imageset
│ │ ├── Contents.json
│ │ └── IMG_7730.jpg
│ ├── ContentView.swift
│ ├── Misc
│ └── Store.storekit
│ ├── Model
│ ├── SongProduct+DataGeneration.swift
│ ├── SongProduct.swift
│ ├── SongProductPurchase.swift
│ └── StoreModel.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── Views
│ ├── SongCellView.swift
│ ├── SongProductProductIcon.swift
│ └── SongProductShop.swift
│ └── meet_storekit_for_swiftuiApp.swift
├── meet-subscriptionstoreview-in-iOS17
├── .DS_Store
├── README.md
├── article-images
│ ├── 1.JPEG
│ ├── 2.JPEG
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ └── 8.png
├── meet-subscriptionstoreview-in-iOS17.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ ├── huangrunhua.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── runhuahuang.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── meet-subscriptionstoreview-in-iOS17.xcscheme
│ └── xcuserdata
│ │ └── huangrunhua.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── meet-subscriptionstoreview-in-iOS17
│ ├── .DS_Store
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── 1-1.png
│ │ └── Contents.json
│ ├── Contents.json
│ └── movie.imageset
│ │ ├── 1-1.png
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── Misc
│ └── Store.storekit
│ ├── Model
│ ├── PassStatus.swift
│ ├── PassStatusModel.swift
│ ├── ProductSubscription.swift
│ └── Store
│ │ └── PassIdentifiers.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── Views
│ ├── SubscriptionShopContent.swift
│ └── SubscriptionShopView.swift
│ └── meet_subscriptionstoreview_in_iOS17App.swift
├── struct-initial-macro
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
│ ├── struct-initial-macro
│ │ └── struct_initial_macro.swift
│ ├── struct-initial-macroClient
│ │ └── main.swift
│ └── struct-initial-macroMacros
│ │ └── struct_initial_macroMacro.swift
├── Tests
│ └── struct-initial-macroTests
│ │ └── struct_initial_macroTests.swift
└── article-imgs
│ └── pic1.png
└── swiftdata-example
├── .DS_Store
├── README.md
├── cover.jpg
├── swiftdata-example.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ └── huangrunhua.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
└── xcuserdata
│ └── huangrunhua.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
└── swiftdata-example
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ └── Contents.json
└── Contents.json
├── ContentView.swift
├── ModelData
├── Note.swift
└── NotePreviewSampleData.swift
├── Preview Content
└── Preview Assets.xcassets
│ └── Contents.json
├── Views
├── NoteEditView.swift
├── NotePreviewCell.swift
└── NoteView.swift
└── swiftdata_exampleApp.swift
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/.DS_Store
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Huang Runhua
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WWDC23 Code Notes
2 |
3 | 
4 |
5 | WWDC23 unfolded before us an extraordinary spectacle, leaving an indelible imprint upon our minds. This magnificent event introduced a multitude of groundbreaking frameworks that propelled developers into the enchanting realms of Swift and SwiftUI, rendering the once formidable journey towards mastery an effortlessly achievable feat. These avant-garde frameworks have inaugurated a new era, emboldening developers to embark upon their coding odyssey with unparalleled fluency and composed composure, harnessing the full potential of Swift and SwiftUI in their quest for innovation and ingenuity.
6 |
7 | Given the ever-evolving landscape of WWDC, which burgeons with new frameworks and transformative advancements each passing year, it is only natural for individuals to gravitate towards areas of personal interest or those challenges encountered during their development endeavors. It is important to note that this project does not aim to comprehensively exemplify every facet of knowledge, but rather selectively focuses on personally intriguing segments that resonate with the individual's curiosity and passion.
8 |
9 | ## Examples
10 |
11 | - [WWDC Swift Macro](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/WWDC)
12 | - [Use Swift macros to initialize a structure](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/struct-initial-macro)
13 | - [First glance at @Observable macro](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/discover-observation-in-swiftui)
14 | - [EventKitUI in iOS 17](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/discover-calendar-and-eventkit)
15 | - [Meet StoreKit for SwiftUI in iOS 17 (non-consumable in-app purchases)](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/meet-storekit-for-swiftui)
16 | - [Meet StoreKit SubscriptionStoreView in iOS 17 (Auto-Renewable Subscription)](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/meet-subscriptionstoreview-in-iOS17)
17 | - [SwiftData demo: Build a Note App](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/swiftdata-example)
18 | - [VisionKit: Lift Subjects from Images in Your App](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/lift-subjects-from-images)
19 |
--------------------------------------------------------------------------------
/WWDC/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/WWDC/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/WWDC/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-syntax",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-syntax.git",
7 | "state" : {
8 | "revision" : "83c2be9f6268e9f67622f130440cf43928c6bfb0",
9 | "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-05-20-a"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/WWDC/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import CompilerPluginSupport
6 |
7 | let package = Package(
8 | name: "WWDC",
9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10 | products: [
11 | // Products define the executables and libraries a package produces, making them visible to other packages.
12 | .library(
13 | name: "WWDC",
14 | targets: ["WWDC"]
15 | ),
16 | .executable(
17 | name: "WWDCClient",
18 | targets: ["WWDCClient"]
19 | ),
20 | ],
21 | dependencies: [
22 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax
23 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
24 | ],
25 | targets: [
26 | // Targets are the basic building blocks of a package, defining a module or a test suite.
27 | // Targets can depend on other targets in this package and products from dependencies.
28 | // Macro implementation that performs the source transformation of a macro.
29 | .macro(
30 | name: "WWDCMacros",
31 | dependencies: [
32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
34 | ]
35 | ),
36 |
37 | // Library that exposes a macro as part of its API, which is used in client programs.
38 | .target(name: "WWDC", dependencies: ["WWDCMacros"]),
39 |
40 | // A client of the library, which is able to use the macro in its own code.
41 | .executableTarget(name: "WWDCClient", dependencies: ["WWDC"]),
42 |
43 | // A test target used to develop the macro implementation.
44 | .testTarget(
45 | name: "WWDCTests",
46 | dependencies: [
47 | "WWDCMacros",
48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
49 | ]
50 | ),
51 | ]
52 | )
53 |
--------------------------------------------------------------------------------
/WWDC/Sources/WWDC/WWDC.swift:
--------------------------------------------------------------------------------
1 | // The Swift Programming Language
2 | // https://docs.swift.org/swift-book
3 |
4 | @attached(member, names: named(init))
5 | /// 定义了`SlopeSubset()`后在使用的时候采用语法`@SlopeSubset`添加关键词
6 | /// `module`用于指明`macro`内部的实现将在哪里定义
7 | /// `type`内的参数值必须和`WWDCMacro`中定义的公开结构体的名字相同
8 | public macro EnumSubset() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
9 |
--------------------------------------------------------------------------------
/WWDC/Sources/WWDCClient/main.swift:
--------------------------------------------------------------------------------
1 | import WWDC
2 |
3 | enum Slope {
4 | case beginnersParadise
5 | case practiceRun
6 | case livingRoom
7 | case olympicRun
8 | case blackBeauty
9 | }
10 |
11 | @EnumSubset
12 | enum EasySlope {
13 | case beginnersParadise
14 | case practiceRun
15 |
16 | var slope: Slope {
17 | switch self {
18 | case .beginnersParadise:
19 | return .beginnersParadise
20 | case .practiceRun:
21 | return .practiceRun
22 | }
23 | }
24 | }
25 |
26 | //@EnumSubset
27 | //struct Skier {}
28 |
--------------------------------------------------------------------------------
/WWDC/Sources/WWDCMacros/WWDCMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import SwiftSyntax
3 | import SwiftSyntaxBuilder
4 | import SwiftSyntaxMacros
5 |
6 | enum SlopeSubsetError: CustomStringConvertible, Error {
7 | case onlyApplicableToEnum
8 |
9 | var description: String {
10 | switch self {
11 | case .onlyApplicableToEnum: return "@EnumSubset can only be applied to an enum"
12 | }
13 | }
14 | }
15 |
16 | public struct SlopeSubsetMacro: MemberMacro {
17 | public static func expansion(
18 | of attribute: AttributeSyntax,
19 | providingMembersOf declaration: some DeclGroupSyntax,
20 | in context: some MacroExpansionContext
21 | ) throws -> [DeclSyntax] {
22 | /// 如果`@SlopeSubset`关键字不是在`enum`前面添加则无效
23 | guard let enumDel = declaration.as(EnumDeclSyntax.self) else {
24 | throw SlopeSubsetError.onlyApplicableToEnum
25 | }
26 |
27 | guard let supersetType = attribute
28 | .attributeName.as(SimpleTypeIdentifierSyntax.self)?
29 | .genericArgumentClause?
30 | .arguments.first?
31 | .argumentType else {
32 | return []
33 | }
34 |
35 | let members = enumDel.memberBlock.members
36 | let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self)}
37 | let elements = caseDecls.flatMap { $0.elements }
38 | let initializer = try InitializerDeclSyntax("init?(_ slope: \(supersetType))") {
39 | try SwitchExprSyntax("switch slope") {
40 | for element in elements {
41 | SwitchCaseSyntax(
42 | """
43 | case .\(element.identifier):
44 | self = .\(element.identifier)
45 | """
46 | )
47 | }
48 | SwitchCaseSyntax("default: return nil")
49 | }
50 | }
51 |
52 | return [DeclSyntax(initializer)]
53 | }
54 | }
55 |
56 | @main
57 | struct WWDCPlugin: CompilerPlugin {
58 | let providingMacros: [Macro.Type] = [
59 | SlopeSubsetMacro.self
60 | ]
61 | }
62 |
--------------------------------------------------------------------------------
/WWDC/Tests/WWDCTests/WWDCTests.swift:
--------------------------------------------------------------------------------
1 | import SwiftSyntaxMacros
2 | import SwiftSyntaxMacrosTestSupport
3 | import XCTest
4 | import WWDCMacros
5 |
6 | let testMacros: [String: Macro.Type] = [
7 | "EnumSubset": SlopeSubsetMacro.self
8 | ]
9 |
10 | final class WWDCTests: XCTestCase {
11 | func testSlopesubset() {
12 | assertMacroExpansion(
13 | """
14 | @EnumSubset
15 | enum Easyslope {
16 | case beginnersParadise
17 | case practiceRun
18 | }
19 | """, expandedSource:
20 | """
21 |
22 | enum Easyslope {
23 | case beginnersParadise
24 | case practiceRun
25 | init?(_ slope: Slope) {
26 | switch slope {
27 | case .beginnersParadise:
28 | self = .beginnersParadise
29 | case .practiceRun:
30 | self = .practiceRun
31 | default:
32 | return nil
33 | }
34 | }
35 | }
36 | """, macros: testMacros)
37 | }
38 |
39 | func testSlopesubsetOnStruct() {
40 | assertMacroExpansion(
41 | """
42 | @EnumSubset
43 | struct skier {
44 | }
45 | """, expandedSource:
46 | """
47 |
48 | struct skier {
49 | }
50 | """,
51 | diagnostics: [
52 | DiagnosticSpec(message: "@EnumSubset can only be applied to an enum", line: 1, column: 1)
53 | ],
54 | macros: testMacros)
55 | }
56 |
57 |
58 | }
59 |
60 |
61 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/cover.jpg
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-calendar-and-eventkit/.DS_Store
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/README.md:
--------------------------------------------------------------------------------
1 | # EventKitUI in iOS 17
2 |
3 | WWDC brings some changes to `EventKit` and `EventKitUI` framework. In iOS 17, one's app can add events to Calendar without prompting the user for access using [`EKEventEditViewController`](https://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller). If the purpose of your app is to create, configure, and present calendar events in an editor UI, consider saving events to Calendar without prompting the user for authorization in your app. In this article I will create a simple app to show how to add events to Calendar in iOS 17. The following picture shows the app we will create later on, it just has one view which shows the ticket's information, and when user tap the **Add to calendar** button, the event editor will show up to enable users to save the event or change some information before saving the event.
4 |
5 | 
6 |
7 | ## Ticket Model
8 |
9 | First of all, we need to define our `Ticket` structure which contains the basic information of one ticket.
10 |
11 | ```swift
12 | struct Ticket: Identifiable {
13 | var id: UUID = UUID()
14 | var title: String
15 | var theater: String
16 | var location: String
17 | var start: String
18 | var end: String
19 | var image: String
20 | }
21 | ```
22 |
23 | Then define a `Ticket` object inside your `ContentView.swift`:
24 |
25 | ```swift
26 | private let ticket: Ticket = Ticket(title: "哆啦A梦:大雄与天空的理想乡",
27 | theater: "Wanda Cinemas",
28 | location: "Orient Cinema Rongchuangmao",
29 | start: "2023-06-10T02:39:32Z",
30 | end: "2023-06-10T04:58:32Z",
31 | image: "movie")
32 | ```
33 |
34 | Before we move on, we need to check the ticket view I create. The ticket view contains several sections:
35 |
36 | - the poster of the movie
37 | - the name of the theater
38 | - the name of the movie
39 | - the opening and closing dates of the movie
40 | - the location of the theater
41 | - the **Add to calendar** button
42 |
43 | Some information can be fetched easily through `ticket` we define, but note the the format of opening dates and the format of closing dates of the movie are different from the `ticket.start` and `ticket.end`. We need to add some member variables in the `Ticket` structure to meet our needs. I am going to use `DateFormatter` to change `String` date to target `Date` date:
44 |
45 | ```swift
46 | struct Ticket: Identifiable {
47 | ...
48 | private var dateFormatter: DateFormatter {
49 | let dfm = DateFormatter()
50 | dfm.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
51 | return dfm
52 | }
53 | private var _startDate: Date? {
54 | if let startDate = dateFormatter.date(from: self.start) {
55 | return startDate
56 | }
57 | return nil
58 | }
59 | private var _endDate: Date? {
60 | if let endDate = dateFormatter.date(from: self.end) {
61 | return endDate
62 | }
63 | return nil
64 | }
65 | }
66 | ```
67 |
68 | Now `_startDate` and `_endDate` store the movie's opening date and ending date in the format of `Date`. It is even better to access the single information such as year, month and day etc. To achieve this, I add `startDate` and `endDate` to `Ticket` which are tuples contains all the information of each date:
69 |
70 | ```swift
71 | struct Ticket: Identifiable {
72 | ...
73 | var startDate: (year: Int, month: Int, day: Int, hour: Int, minute: Int)? {
74 | if let components = _startDate?.get(.day, .month, .year, .hour, .minute) {
75 | if let year = components.year,
76 | let day = components.day,
77 | let month = components.month,
78 | let hour = components.hour,
79 | let minute = components.minute {
80 | return (year, month, day, hour, minute)
81 | }
82 | }
83 | return nil
84 | }
85 | var endDate: (year: Int, month: Int, day: Int, hour: Int, minute: Int)? {
86 | if let components = _endDate?.get(.day, .month, .year, .hour, .minute) {
87 | if let year = components.year,
88 | let day = components.day,
89 | let month = components.month,
90 | let hour = components.hour,
91 | let minute = components.minute {
92 | return (year, month, day, hour, minute)
93 | }
94 | }
95 | return nil
96 | }
97 | }
98 |
99 | extension Date {
100 | func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents {
101 | return calendar.dateComponents(Set(components), from: self)
102 | }
103 |
104 | func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int {
105 | return calendar.component(component, from: self)
106 | }
107 | }
108 | ```
109 |
110 | Now we can access the single information such as year by using the syntax `ticket.startDate.year`.
111 |
112 | ## Ticket View
113 |
114 | Ticket view is easy to implement, here I just give the full code of ticket view:
115 |
116 | ```swift
117 | struct ContentView: View {
118 | private let ticket: Ticket = Ticket(title: "哆啦A梦:大雄与天空的理想乡",
119 | theater: "Wanda Cinemas",
120 | location: "Orient Cinema Rongchuangmao",
121 | start: "2023-06-10T02:39:32Z",
122 | end: "2023-06-10T04:58:32Z",
123 | image: "movie")
124 |
125 | var body: some View {
126 | ZStack(alignment: .bottom, content: {
127 | Image(ticket.image)
128 | .resizable()
129 | .aspectRatio(contentMode: .fit)
130 | HStack {
131 | VStack(alignment: .leading, spacing: 1) {
132 | Text(ticket.theater)
133 | .foregroundStyle(.blue)
134 | .bold()
135 | Text(ticket.title)
136 | .font(.system(size: 20))
137 | if let startDate = ticket.startDate, let endDate = ticket.endDate {
138 | Text("\(startDate.month)月\(startDate.day)日 \(startDate.hour):\(startDate.minute)-\(endDate.hour):\(endDate.minute)")
139 | }
140 | HStack {
141 | Image(systemName: "mappin")
142 | .foregroundStyle(.red)
143 | Text(ticket.location)
144 | .foregroundStyle(.gray)
145 | }
146 | .padding(.top, 7)
147 | .font(.system(size: 16))
148 |
149 | HStack {
150 | Image(systemName: "calendar")
151 | .foregroundStyle(.blue)
152 | Button("Add to calendar") {
153 | /// Add to calendar...
154 | }
155 | }
156 | .padding(.top, 7)
157 | }
158 | Spacer()
159 | }
160 | .padding()
161 | .frame(width: 350)
162 | .background(Color.white)
163 | })
164 | .frame(width: 350)
165 | .clipShape(RoundedRectangle(cornerRadius: 10))
166 | .shadow(radius: 5)
167 | }
168 | }
169 | ```
170 |
171 | ## Save events using EventKitUI
172 |
173 | On iOS, the EventKitUI framework is to show calendar and reminder information to the user modally. EventKitUI provides view controllers for viewing and editing calendar and reminder information, choosing which calendar to view, and for determining whether to present calendars as read-only or readable and writeable. Since we don't have SwiftUI-version EventKitUI we have to convert a `UIViewController` to `View`.
174 |
175 | ### What is new in Calendar
176 |
177 | As I said earlier, In iOS 17, your app can add events to Calendar without prompting the user for access using [`EKEventEditViewController`](https://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller). **It means you don't have to provide the NSCalendarsUsageDescription key (this key has been deprecated in iOS 17.0) or any other key in `info.plist`**. And app should only request the specific level of access it requires to complete its calendar data tasks. The iOS 17 SDK also introduces new calendar usage description strings, the ability to add events to Calendar without prompting the user for access, and a new write-only access. Since we only add events without any key, I am not going to talk those details here, you can see [Accessing the event store](https://developer.apple.com/documentation/eventkit/accessing_the_event_store) for details. Now let's see how to create `EventEditViewController`.
178 |
179 | ### EventEditViewController
180 |
181 | To make `EKEventEditViewController` work in SwiftUI, we need to turn to `UIViewControllerRepresentable` for help. Our `UIViewControllerType` is defined to be `EKEventEditViewController`.
182 |
183 | ```swift
184 | import EventKitUI
185 |
186 | struct EventEditViewController: UIViewControllerRepresentable {
187 | typealias UIViewControllerType = EKEventEditViewController
188 | func makeUIViewController(context: Context) -> EKEventEditViewController {
189 |
190 | }
191 | func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {}
192 | }
193 | ```
194 |
195 | Adding an event with `EventKitUI` is a **four-step** process:
196 |
197 | 1. Create an event store.
198 | 2. Create an event and fill in the details.
199 | 3. Create a view controller configured to edit the event.
200 | 4. Present the view controller.
201 |
202 | Create an event store is easy, just one line of code:
203 |
204 | ```swift
205 | private let store = EKEventStore()
206 | ```
207 |
208 | Creating an `EKEvent` object instance is a more complicated process, because we need to add detailed information to the object instance. Here I created a private variable named `event`, which converts the information in the variable `ticket` into `EKEvent` type:
209 |
210 | ```swift
211 | struct EventEditViewController: UIViewControllerRepresentable {
212 | let ticket: Ticket
213 | private var event: EKEvent {
214 | let event = EKEvent(eventStore: store)
215 | event.title = ticket.title
216 | if let startDate = ticket.startDate, let endDate = ticket.endDate {
217 | let startDateComponents = DateComponents(year: startDate.year,
218 | month: startDate.month,
219 | day: startDate.day,
220 | hour: startDate.hour,
221 | minute: startDate.minute)
222 | event.startDate = Calendar.current.date(from: startDateComponents)!
223 | let endDateComponents = DateComponents(year: endDate.year,
224 | month: endDate.month,
225 | day: endDate.day,
226 | hour: endDate.hour,
227 | minute: endDate.minute)
228 | event.endDate = Calendar.current.date(from: endDateComponents)!
229 | event.location = ticket.location
230 | event.notes = "Don't forget to bring popcorn🍿️!"
231 | }
232 | return event
233 | }
234 | ...
235 | }
236 | ```
237 |
238 | Every event needs a title. The title is used in many places including widgets and notifications, so keep it simple. The most important properties are the start and end date. Use date components to make the start date and end date. Set a location to let people know where the event takes place. Including a full address or using a MapKit handle will enable features like Maps suggestions and Time to Leave alerts. Finally, I add some notes to provide some extra detail.
239 |
240 | Now you've set the event properties, the next step is to create the `EKEventEditViewController`. Assign the event and event store properties. The code is written inside method `makeUIViewController(context:)`.
241 |
242 | ```swift
243 | func makeUIViewController(context: Context) -> EKEventEditViewController {
244 | let eventEditViewController = EKEventEditViewController()
245 | eventEditViewController.event = event
246 | eventEditViewController.eventStore = store
247 | return eventEditViewController
248 | }
249 | ```
250 |
251 | ### Add to Calendar
252 |
253 | Now back to our ticket view, we have some left work to finish. Add a variable called `showEventEditView` which is used to show the `EventEditViewController`:
254 |
255 | ```swift
256 | @State private var showEventEditView: Bool = false
257 | ```
258 |
259 | When user taps the **Add to Calendar** button, `showEventEditView` should become `true` and then shows the `EventEditViewController`:
260 |
261 | ```swift
262 | Button("Add to calendar") {
263 | self.showEventEditView.toggle()
264 | }
265 | .sheet(isPresented: $showEventEditView, content: {
266 | EventEditViewController(ticket: self.ticket)
267 | })
268 | ```
269 |
270 | Now when we tap the button, the event edit view should present. However, you may find that when tapping cancel or add button, the event edit view won't dismiss. That is because the calendar edits happen out of process, inspecting the properties of the dismissed controller can help us dismiss the view.
271 |
272 | ### Enable Dismiss
273 |
274 | Since `UIViewControllerRepresentable` doesn’t automatically communicate changes occurring within our view controller to other parts of our SwiftUI interface. When we want our view controller to coordinate with other SwiftUI views, we must provide a [`Coordinator`](doc://com.apple.documentation/documentation/swiftui/nsviewcontrollerrepresentable/coordinator) instance to facilitate those interactions.
275 |
276 | ```swift
277 | struct EventEditViewController: UIViewControllerRepresentable {
278 | @Environment(\.presentationMode) var presentationMode
279 | ...
280 | func makeCoordinator() -> Coordinator {
281 | return Coordinator(self)
282 | }
283 |
284 | class Coordinator: NSObject, EKEventEditViewDelegate {
285 | var parent: EventEditViewController
286 |
287 | init(_ controller: EventEditViewController) {
288 | self.parent = controller
289 | }
290 |
291 | func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
292 | parent.presentationMode.wrappedValue.dismiss()
293 | }
294 | }
295 | }
296 | ```
297 |
298 | Finally in method `makeUIViewController` add the following code:
299 |
300 | ```swift
301 | func makeUIViewController(context: Context) -> EKEventEditViewController {
302 | ...
303 | eventEditViewController.editViewDelegate = context.coordinator
304 | return eventEditViewController
305 | }
306 | ```
307 |
308 | Run your project and change the event date for name to see what happens.
309 |
310 | ## Source Code
311 |
312 | You can find the source code on [GitHub](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/discover-calendar-and-eventkit).
313 |
314 | > If you think this article is helpful, you can support me by downloading my first Mac App which named [FilerApp](https://huangrunhua.github.io/FilerApp/) on the [Mac App Store](https://apps.apple.com/us/app/filerapp/id1626627609?mt=12&itsct=apps_box_link&itscg=30200). FilerApp is a Finder extension for your Mac which enables you to easily create files in supported formats anywhere on the system. It is free and useful for many people. Hope you like it.
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-calendar-and-eventkit/discover-calendar-and-eventkit.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit.xcodeproj/xcuserdata/huangrunhua.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | discover-calendar-and-eventkit.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Assets.xcassets/movie.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "p2891257290.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Assets.xcassets/movie.imageset/p2891257290.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Assets.xcassets/movie.imageset/p2891257290.jpg
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // discover-calendar-and-eventkit
4 | //
5 | // Created by Huang Runhua on 6/9/23.
6 | //
7 |
8 | import SwiftUI
9 | ///
10 | /// In iOS 17, an app with write-only access can create and save events to Calendar, display
11 | /// events using EKEventEditViewController, and allow the user to select another calendar using
12 | /// EKCalendarChooser. If your app needs to write data directly, consider implementing
13 | /// write-only access in your app following these steps:
14 | /// 1. Add the `NSCalendarsWriteOnlyAccessUsageDescription` key to
15 | /// the `Info.plist` file of the target building your app.
16 | /// 2. To request write-only access to events, use `requestWriteOnlyAccessToEvents(completion:)`
17 | /// or `requestWriteOnlyAccessToEvents()`.
18 | struct ContentView: View {
19 |
20 | /// Orient Cinema Rongchuangmao F B1 Guangying Shiji North Side, Huangdao, Qingdao, Shandong China
21 | private let ticket: Ticket = Ticket(title: "哆啦A梦:大雄与天空的理想乡",
22 | theater: "Wanda Cinemas",
23 | location: "Orient Cinema Rongchuangmao",
24 | start: "2023-06-10T02:39:32Z",
25 | end: "2023-06-10T04:58:32Z",
26 | image: "movie")
27 |
28 | @State private var showEventEditView: Bool = false
29 |
30 | var body: some View {
31 | ZStack(alignment: .bottom, content: {
32 | Image(ticket.image)
33 | .resizable()
34 | .aspectRatio(contentMode: .fit)
35 | HStack {
36 | VStack(alignment: .leading, spacing: 1) {
37 | Text(ticket.theater)
38 | .foregroundStyle(.blue)
39 | .bold()
40 | Text(ticket.title)
41 | .font(.system(size: 20))
42 | if let startDate = ticket.startDate, let endDate = ticket.endDate {
43 | Text("\(startDate.month)月\(startDate.day)日 \(startDate.hour):\(startDate.minute)-\(endDate.hour):\(endDate.minute)")
44 | }
45 | HStack {
46 | Image(systemName: "mappin")
47 | .foregroundStyle(.red)
48 | Text(ticket.location)
49 | .foregroundStyle(.gray)
50 | }
51 | .padding(.top, 7)
52 | .font(.system(size: 16))
53 |
54 | HStack {
55 | Image(systemName: "calendar")
56 | .foregroundStyle(.blue)
57 | Button("Add to calendar") {
58 | self.showEventEditView.toggle()
59 | }
60 | .sheet(isPresented: $showEventEditView, content: {
61 | EventEditViewController(ticket: self.ticket)
62 | })
63 | }
64 | .padding(.top, 7)
65 | }
66 | Spacer()
67 | }
68 | .padding()
69 | .frame(width: 350)
70 | .background(Color.white)
71 | })
72 | .frame(width: 350)
73 | .clipShape(RoundedRectangle(cornerRadius: 10))
74 | .shadow(radius: 5)
75 | }
76 | }
77 |
78 | #Preview {
79 | ContentView()
80 | }
81 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/EventEditView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EventEditView.swift
3 | // discover-calendar-and-eventkit
4 | //
5 | // Created by Huang Runhua on 6/9/23.
6 | //
7 |
8 | import SwiftUI
9 | import EventKitUI
10 |
11 | struct EventEditViewController: UIViewControllerRepresentable {
12 | @Environment(\.presentationMode) var presentationMode
13 | typealias UIViewControllerType = EKEventEditViewController
14 |
15 | let ticket: Ticket
16 |
17 | private let store = EKEventStore()
18 | private var event: EKEvent {
19 | let event = EKEvent(eventStore: store)
20 | event.title = ticket.title
21 | if let startDate = ticket.startDate, let endDate = ticket.endDate {
22 | let startDateComponents = DateComponents(year: startDate.year,
23 | month: startDate.month,
24 | day: startDate.day,
25 | hour: startDate.hour,
26 | minute: startDate.minute)
27 | event.startDate = Calendar.current.date(from: startDateComponents)!
28 | let endDateComponents = DateComponents(year: endDate.year,
29 | month: endDate.month,
30 | day: endDate.day,
31 | hour: endDate.hour,
32 | minute: endDate.minute)
33 | event.endDate = Calendar.current.date(from: endDateComponents)!
34 | event.location = ticket.location
35 | event.notes = "Don't forget to bring popcorn🍿️!"
36 | }
37 | return event
38 | }
39 |
40 | func makeUIViewController(context: Context) -> EKEventEditViewController {
41 | let eventEditViewController = EKEventEditViewController()
42 | eventEditViewController.event = event
43 | eventEditViewController.eventStore = store
44 | eventEditViewController.editViewDelegate = context.coordinator
45 | return eventEditViewController
46 | }
47 |
48 | func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {}
49 |
50 | func makeCoordinator() -> Coordinator {
51 | return Coordinator(self)
52 | }
53 |
54 | class Coordinator: NSObject, EKEventEditViewDelegate {
55 | var parent: EventEditViewController
56 |
57 | init(_ controller: EventEditViewController) {
58 | self.parent = controller
59 | }
60 |
61 | func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
62 | parent.presentationMode.wrappedValue.dismiss()
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/Ticket.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Ticket.swift
3 | // discover-calendar-and-eventkit
4 | //
5 | // Created by Huang Runhua on 6/9/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Ticket: Identifiable {
11 | var id: UUID = UUID()
12 | var title: String
13 | var theater: String
14 | var location: String
15 | var start: String
16 | var end: String
17 | var image: String
18 |
19 | var startDate: (year: Int, month: Int, day: Int, hour: Int, minute: Int)? {
20 | if let components = _startDate?.get(.day, .month, .year, .hour, .minute) {
21 | if let year = components.year,
22 | let day = components.day,
23 | let month = components.month,
24 | let hour = components.hour,
25 | let minute = components.minute {
26 | return (year, month, day, hour, minute)
27 | }
28 | }
29 | return nil
30 | }
31 |
32 | var endDate: (year: Int, month: Int, day: Int, hour: Int, minute: Int)? {
33 | if let components = _endDate?.get(.day, .month, .year, .hour, .minute) {
34 | if let year = components.year,
35 | let day = components.day,
36 | let month = components.month,
37 | let hour = components.hour,
38 | let minute = components.minute {
39 | return (year, month, day, hour, minute)
40 | }
41 | }
42 | return nil
43 | }
44 |
45 | private var dateFormatter: DateFormatter {
46 | let dfm = DateFormatter()
47 | dfm.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
48 | return dfm
49 | }
50 |
51 | private var _startDate: Date? {
52 | if let startDate = dateFormatter.date(from: self.start) {
53 | return startDate
54 | }
55 | return nil
56 | }
57 |
58 | private var _endDate: Date? {
59 | if let endDate = dateFormatter.date(from: self.end) {
60 | return endDate
61 | }
62 | return nil
63 | }
64 | }
65 |
66 |
67 | extension Date {
68 | func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents {
69 | return calendar.dateComponents(Set(components), from: self)
70 | }
71 |
72 | func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int {
73 | return calendar.component(component, from: self)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/discover-calendar-and-eventkit/discover_calendar_and_eventkitApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // discover_calendar_and_eventkitApp.swift
3 | // discover-calendar-and-eventkit
4 | //
5 | // Created by Huang Runhua on 6/9/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct discover_calendar_and_eventkitApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/discover-calendar-and-eventkit/images/IMG_7703.JPEG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-calendar-and-eventkit/images/IMG_7703.JPEG
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-observation-in-swiftui/.DS_Store
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/README.md:
--------------------------------------------------------------------------------
1 | # First glance at @Observable macro
2 |
3 | WWDC23 brings enormous magical new features in Swift. In Tuesday's video [Discover Observation in SwiftUI](https://developer.apple.com/wwdc23/10149) Philipe taught us how the `@Observable` macro can help simplify models and improve app's performance. In this article, I want to go over some concepts about `Observable` in that video and then shows some examples of how to use `@Bindable` and the new `@Observable` macro.
4 |
5 | ## How does Observable Simplify Models?
6 |
7 | Before the advent of `@Observable` if we want to declare a data model, we usually enforce our data model to comply `ObservableObject` protocol. Inside the data model we have to declare a number of properties that were marked with the `@Published` property wrapper. For example, here is a music model I declared:
8 |
9 | ```swift
10 | public class MusicModelOOversion: ObservableObject {
11 | @Published var musics: [Music] = [
12 | Music(title: "Spirals", singer: "Nick Leng"),
13 | Music(title: "Rocky", singer: "Still Woozy"),
14 | Music(title: "Time Square", singer: "Jam City"),
15 | Music(title: "The Only One", singer: "Phoenix"),
16 | Music(title: "Away X5", singer: "Yaeji")
17 | ]
18 |
19 | var musicCount: Int {
20 | musics.count
21 | }
22 | }
23 | ```
24 |
25 | `Music` is a struct which defines in the following way:
26 |
27 | ```swift
28 | struct Music: Identifiable {
29 | var id: UUID = UUID()
30 | var title: String
31 | var singer: String
32 | }
33 | ```
34 |
35 | The model data has a list property called `musics` which shows all the musics together with a property called `musicCount` which shows the number of the music declared in `musics`. The counterpart version rewritten by `Observable` shows below:
36 |
37 | ```swift
38 | import Observation
39 |
40 | @Observable public class MusicModel {
41 | var musics: [Music] = [
42 | Music(title: "Spirals", singer: "Nick Leng"),
43 | Music(title: "Rocky", singer: "Still Woozy"),
44 | Music(title: "Time Square", singer: "Jam City"),
45 | Music(title: "The Only One", singer: "Phoenix"),
46 | Music(title: "Away X5", singer: "Yaeji")
47 | ]
48 | var musicCount: Int {
49 | musics.count
50 | }
51 | }
52 | ```
53 |
54 | Changing over to the `@Observable` macro was pretty easy. All we needed to do is remove the conformance to `ObservableObject`, remove the `@Published`, and mark it with the `@Observable` macro. When it comes to the views, the OO-version (OO means ObservableObject) needs to declare `@ObservedObject` or `@EnvironmentObject` property wrapper before any variable.
55 |
56 | ```swift
57 | struct ContentView_OOVersion: View {
58 | @ObservedObject var model: MusicModelOOversion
59 | var body: some View {
60 | List {
61 | Section {
62 | ForEach(model.musics) { music in
63 | VStack(alignment: .leading) {
64 | Text(music.title)
65 | Text(music.singer)
66 | .font(.system(size: 15))
67 | .foregroundStyle(.gray)
68 | }
69 | }
70 | Button("Add new music") {
71 | model.addMusic()
72 | }
73 | } header: {
74 | Text("Musics")
75 | } footer: {
76 | Text("\(model.musicCount) songs")
77 | }
78 | }
79 | }
80 | }
81 | ```
82 |
83 | However, O-version (O means Observable) gives us a more graceful coding style. For `@ObservedObject` property wrapper we just remove it and everything is the same.
84 |
85 | ```swift
86 | struct ContentView: View {
87 | var model: MusicModel
88 | var body: some View {
89 | List {
90 | Section {
91 | ForEach(model.musics) { music in
92 | VStack(alignment: .leading) {
93 | Text(music.title)
94 | Text(music.singer)
95 | .font(.system(size: 15))
96 | .foregroundStyle(.gray)
97 | }
98 | }
99 | Button("Add new music") {
100 | model.addMusic()
101 | }
102 | } header: {
103 | Text("Musics")
104 | } footer: {
105 | Text("\(model.musicCount) songs")
106 | }
107 | }
108 | }
109 | }
110 | ```
111 |
112 | If there are a lot of models, by using OO-version, the code would be:
113 |
114 | ```swift
115 | @ObservedObject var musicModel: MusicModel
116 | @ObservedObject var songModel: SongModel
117 | @ObservedObject var singerModel: SingerModel
118 | /// And so on...
119 | ```
120 |
121 | Compare with the OO-version, the O-version looks not so complicated and it looks just like declaring ordinary variables:
122 |
123 | ```swift
124 | var musicModel: MusicModel
125 | var songModel: SongModel
126 | var singerModel: SingerModel
127 | @Environment(AccountStore.self) private var accountStore
128 | /// And so on...
129 | ```
130 |
131 | When it coms to `@EnvironmentObject` you can simply it to `@Environment`:
132 |
133 | ```swift
134 | /// OO-version
135 | @EnvironmentObject private var accountStore: AccountStore
136 | /// O-version
137 | @Environment(AccountStore.self) private var accountStore
138 | ```
139 |
140 | ## What is @Bindable
141 |
142 | The newest of the family of property wrappers is `@Bindable`. The former version `@Binding` is highly recommended by Apple to be replaced by `@Bindable` in the newer version of SwiftUI. The bindable property wrapper is really lightweight. All it does is allow bindings to be created from that type. Getting binding out of a bindable wrapped property is really easy. Just use the $ syntax to get the binding to that property. Most often, this will be bindings to observable types.
143 |
144 | If we attach `@Observable` to a class then we can attach `@Bindable` to any object it create.
145 |
146 | ```swift
147 | @Observable class Article {
148 | var title: String = ""
149 | var subtitle: String = ""
150 | }
151 |
152 | struct ArticleEditView: View {
153 | @Bindable var article: Article
154 | var body: some View {
155 | VStack {
156 | TextField("Title", text: $article.title)
157 | TextField("Subtitle", text: $article.subtitle)
158 | }.padding()
159 | }
160 | }
161 | ```
162 |
163 | The above code create an `Article` class with `@Observable` attached to it. `Article` contains two properties: `title` and `subtitle`. The code also create an `ArticleEditView` which enables users to change the title and subtitle of an article. Here I use `TextField ` to give users choice to change the basic information of an article. That TextField takes a binding. It reads from the binding to populate the value of the TextField, but it also writes back to the binding when the user changes the value. To make bindings to the article, all we need to do is use the `@Bindable` property wrapper on the article property. The property wrapper annotation allows us to use the `$article.title` or `$article.subtitle` syntax and creates a binding when used.
164 |
165 | ## When to Use @State, @Bindable and @Environment
166 |
167 | SwiftUI now only focus on the three primary property wrappers: `@State`, `@Environment` and `@Bindable`. There are only three questions you need to answer for using observable models in SwiftUI. Does this model need to be state of the view itself? If so, use `@State`. Does this model need to be part of the global environment of the application? If so, use `@Environment`. Does this model just need bindings? If so, use the new `@Bindable`. And if none of these questions have the answer as yes, just use the model as a property of your view.
168 |
169 | 
170 |
171 | ## Source Code
172 |
173 | You can find the source code on [Github](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/discover-observation-in-swiftui).
174 |
175 | ## Supports Me
176 |
177 | If you think this article is helpful, you can support me by downloading my first Mac App which named [FilerApp](https://huangrunhua.github.io/FilerApp/) on the [Mac App Store](https://apps.apple.com/us/app/filerapp/id1626627609?mt=12&itsct=apps_box_link&itscg=30200). FilerApp is a Finder extension for your Mac which enables you to easily create files in supported formats anywhere on the system. It is free and useful for many people. Hope you like it.
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-observation-in-swiftui/discover-observation-in-swiftui.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui.xcodeproj/xcuserdata/huangrunhua.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | discover-observation-in-swiftui.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/OO-version-example/ContentView_OOVersion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView_OOVersion.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView_OOVersion: View {
11 |
12 | @ObservedObject var model: MusicModelOOversion
13 |
14 | var body: some View {
15 | List {
16 | Section {
17 | ForEach(model.musics) { music in
18 | VStack(alignment: .leading) {
19 | Text(music.title)
20 | Text(music.singer)
21 | .font(.system(size: 15))
22 | .foregroundStyle(.gray)
23 | }
24 | }
25 | Button("Add new music") {
26 | model.addMusic()
27 | }
28 | } header: {
29 | Text("Musics")
30 | } footer: {
31 | Text("\(model.musicCount) songs")
32 | }
33 | }
34 | }
35 | }
36 |
37 | #Preview {
38 | ContentView_OOVersion(model: MusicModelOOversion())
39 | }
40 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/OO-version-example/ModelData_OOversion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelData_OOversion.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | final class MusicModelOOversion: ObservableObject {
11 | @Published var musics: [Music] = [
12 | Music(title: "Spirals", singer: "Nick Leng"),
13 | Music(title: "Rocky", singer: "Still Woozy"),
14 | Music(title: "Time Square", singer: "Jam City"),
15 | Music(title: "The Only One", singer: "Phoenix"),
16 | Music(title: "Away X5", singer: "Yaeji")
17 | ]
18 |
19 | var musicCount: Int {
20 | musics.count
21 | }
22 |
23 | init() {}
24 |
25 | func addMusic() {
26 | musics.append(Music(title: "Cold Winter", singer: "July"))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/bindable-example/Article.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Article.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import Foundation
9 | import Observation
10 |
11 | @Observable
12 | class Article {
13 | var title: String = ""
14 | var subtitle: String = ""
15 | }
16 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/bindable-example/ArticleEditView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArticleEditView.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ArticleEditView: View {
11 |
12 | // If Article is not conforms to Observable
13 | // Xcode will throw an error: 'init(wrappedValue:)'
14 | // is unavailable: The wrapped value must be an
15 | // object that conforms to Observable
16 | @Bindable var article: Article
17 |
18 | // @Binding var article: Article
19 | var body: some View {
20 | VStack {
21 | TextField("Title", text: $article.title)
22 | TextField("Subtitle", text: $article.subtitle)
23 | }.padding()
24 | }
25 | }
26 |
27 | #Preview {
28 | ArticleEditView(article: Article())
29 | // ArticleEditView(article: .constant(Article()))
30 | }
31 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/discover_observation_in_swiftuiApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // discover_observation_in_swiftuiApp.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct discover_observation_in_swiftuiApp: App {
12 | @StateObject private var modelData = MusicModelOOversion()
13 | var body: some Scene {
14 | WindowGroup {
15 | // ContentView(model: MusicModel())
16 | ContentView_OOVersion(model: modelData)
17 | // AccountView()
18 | // .environment(Account())
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/environment-example/Account.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Account.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import Foundation
9 | import Observation
10 |
11 | @Observable class Account {
12 | var userName: String = "Joker Hook"
13 | }
14 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/environment-example/AccountView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccountView.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AccountView: View {
11 |
12 | @Environment(Account.self) var account
13 |
14 | var body: some View {
15 | if !(account.userName == "") {
16 | HStack {
17 | Text(account.userName)
18 | Button("Log out") {
19 | print(account.userName)
20 | account.userName = ""
21 | }
22 | }
23 | } else {
24 | Button("Login") {
25 | account.userName = "Joker Hook"
26 | }
27 | }
28 | }
29 | }
30 |
31 | #Preview {
32 | AccountView()
33 | // Also need this.
34 | .environment(Account())
35 | }
36 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/observable-in-array-example/Book.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Book.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import Foundation
9 | import Observation
10 |
11 | /// Storing `@Observable` types in Array
12 | @Observable class Book: Identifiable {
13 | var id = UUID()
14 | var title: String = ""
15 | var loved: Bool = false
16 |
17 | init(title: String) {
18 | self.title = title
19 | }
20 | }
21 |
22 | //class Book: Identifiable {
23 | // var id = UUID()
24 | // var title: String = ""
25 | // var loved: Bool = false
26 | //
27 | // init(title: String) {
28 | // self.title = title
29 | // }
30 | //}
31 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/observable-in-array-example/BooksList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BooksList.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Storing `@Observable` types in Array
11 | struct BooksList: View {
12 |
13 | var books: [Book] = [
14 | Book(title: "Great Expectations"),
15 | Book(title: "Little Women"),
16 | Book(title: "Pride and Prejudice")
17 | ]
18 |
19 | var body: some View {
20 | List {
21 | ForEach(books) { book in
22 | HStack {
23 | Text(book.title)
24 | Spacer()
25 | Button(action: {
26 | // If there is no `@Observable`
27 | // attached to Book class, when tapping
28 | // the love button the view won't auto update
29 | book.loved.toggle()
30 | }, label: {
31 | Image(systemName: book.loved ? "heart.fill": "heart")
32 | })
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
39 | #Preview {
40 | BooksList()
41 | }
42 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/state-example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | var model: MusicModel
13 |
14 | @State private var showAddMusicView: Bool = false
15 | @State private var musicToAdd: Music = Music(title: "", singer: "")
16 |
17 | var body: some View {
18 | List {
19 | Section {
20 | ForEach(model.musics) { music in
21 | VStack(alignment: .leading) {
22 | Text(music.title)
23 | Text(music.singer)
24 | .font(.system(size: 15))
25 | .foregroundStyle(.gray)
26 | }
27 | }
28 | Button("Add new music") {
29 | showAddMusicView.toggle()
30 | }
31 | .sheet(isPresented: $showAddMusicView, content: {
32 | TextField("Title", text: $musicToAdd.title)
33 | .padding([.leading, .trailing])
34 | TextField("Singer", text: $musicToAdd.singer)
35 | .padding([.leading, .trailing])
36 | Button("Save") {
37 | model.musics.append(musicToAdd)
38 | showAddMusicView.toggle()
39 | }
40 | Button("Cancel") { showAddMusicView.toggle() }
41 | })
42 | } header: {
43 | Text("Musics")
44 | } footer: {
45 | Text("\(model.musicCount) songs")
46 | }
47 | }
48 | }
49 | }
50 |
51 | #Preview {
52 | ContentView(model: MusicModel())
53 | }
54 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/state-example/ModelData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelData.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import SwiftUI
9 | import Observation
10 |
11 | @Observable class MusicModel {
12 | var musics: [Music] = [
13 | Music(title: "Spirals", singer: "Nick Leng"),
14 | Music(title: "Rocky", singer: "Still Woozy"),
15 | Music(title: "Time Square", singer: "Jam City"),
16 | Music(title: "The Only One", singer: "Phoenix"),
17 | Music(title: "Away X5", singer: "Yaeji")
18 | ]
19 | var musicCount: Int {
20 | musics.count
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/discover-observation-in-swiftui/state-example/Music.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Music.swift
3 | // discover-observation-in-swiftui
4 | //
5 | // Created by Huang Runhua on 6/8/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Music: Identifiable {
11 | var id: UUID = UUID()
12 | var title: String
13 | var singer: String
14 | }
15 |
--------------------------------------------------------------------------------
/discover-observation-in-swiftui/pic1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/discover-observation-in-swiftui/pic1.jpg
--------------------------------------------------------------------------------
/lift-subjects-from-images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/lift-subjects-from-images/.DS_Store
--------------------------------------------------------------------------------
/lift-subjects-from-images/README.md:
--------------------------------------------------------------------------------
1 | # Lift Subjects from Images in Your App
2 |
3 | Discover how you can easily pull the subject of an image from its background in your apps.
4 |
5 | 
6 |
7 | WWDC23 brings a lot of updates to enable developers create greatest user experience ever. Now VisionKit enables users lift subjects from images. With just a few lines of code, you can help users easily pull the subject of an image from its background in your apps.
8 |
9 | In this article, I will share my experience of how to get the most out of VisionKit (lift subjects, Live Text, recognize machine-readable codes, such as QR codes and visual look up and so on) inside your SwiftUI project.
10 |
11 | ## Build a Main View
12 |
13 | Create a new Xcode project and we will focus on the `ContentView.swift` file now. The following code is similar to the code putting in my previous article: [WWDC22: Enabling Live Text Interactions With Images in SwiftUI](https://medium.com/better-programming/enabling-live-text-interactions-with-images-in-swiftui-5dd1d7f1676). I suppose that you have the basic knowledge of SwiftUI, if no please check Apple’s official document: [Introducing SwiftUI](https://developer.apple.com/tutorials/swiftui).
14 |
15 | ```swift
16 | import SwiftUI
17 | import VisionKit
18 |
19 | struct ContentView: View {
20 | @State private var deviceSupportLiveText = false
21 | @State private var showDeviceNotCapacityAlert = false
22 | @State private var showLiveTextView = false
23 |
24 | var body: some View {
25 | Button {
26 | if deviceSupportLiveText {
27 | self.showLiveTextView = true
28 | } else {
29 | self.showDeviceNotCapacityAlert = true
30 | }
31 | } label: {
32 | Text("Pick an Image")
33 | .foregroundColor(.white)
34 | .padding()
35 | .frame(width: 300, height: 50)
36 | .background(Color.blue)
37 | .cornerRadius(10)
38 | }
39 | .alert("Live Text Unavailable", isPresented: $showDeviceNotCapacityAlert, actions: {})
40 | .sheet(isPresented: $showLiveTextView, content: {
41 | LiveTextInteractionView()
42 | })
43 | .onAppear {
44 | // Do something when view appears
45 | }
46 | }
47 | }
48 | ```
49 |
50 | The above code generate a view that only contains a `Button` view, its function is to present the Live Text view and let users copy or do something else to the detected texts or machine readable code as well as lift subjects. However, not all device is capable with that function, according to Apple:
51 |
52 | > For iOS apps, Live Text is only available on devices with the A12 Bionic chip and later.
53 |
54 | Fortunately, Apple provides us a new API to check whether the device supports Live Text. If the device isn’t support Live Text, when attempting to tap the button to present the Live Text view, the app will present an alert that illustrates device is not capable with the Live Text.
55 |
56 | 
57 |
58 | # Build a Live Text View
59 |
60 | Live Text view contains all the features that is need to perform actions with text, QR codes and subjects that appear in images.
61 |
62 | Create a new SwiftUI file named `ImageLiftView.swift`, and add the following code to the file:
63 |
64 | ```swift
65 | struct ImageLiftView: View {
66 | @Environment(\.presentationMode) var presentationMode
67 | var body: some View {
68 | NavigationView {
69 | ImageLift(imageName: "dog")
70 | .toolbar {
71 | ToolbarItem(placement: .navigationBarLeading) {
72 | Button {
73 | self.presentationMode.wrappedValue.dismiss()
74 | } label: {
75 | Text("Cancel")
76 | }
77 | }
78 | }
79 | .interactiveDismissDisabled(true)
80 | }
81 | }
82 | }
83 | ```
84 |
85 | ## Check whether the device supports Live Text
86 |
87 | Before showing a Live Text interface in your app, check whether the device supports Live Text. If the `ImageAnalyzer` `isSupported` property is `true`, show the Live Text interface.
88 |
89 | In your `ContentView` add the following check sentence inside your `onAppear`code:
90 |
91 | ```swift
92 | .onAppear {
93 | self.deviceSupportLiftSubject = ImageAnalyzer.isSupported
94 | }
95 | ```
96 |
97 | This will enable your app to check whether the device supports Live Text as soon as the app is launched.
98 |
99 | ## Add a Image interaction object to your view in iOS
100 |
101 | This article only contains the instructions on how to implement the API inside an iOS or iPadOS app, so I will not analyze the API in macOS.
102 |
103 | To embed a `UIView` inside a SwiftUI view, we need `UIViewRepresentable` to help us.
104 |
105 | Create a new swift file named `ImageLift.swift`, and add the following code:
106 |
107 | ```swift
108 | import VisionKit
109 |
110 | @MainActor
111 | struct ImageLift: UIViewRepresentable {
112 | var imageName: String
113 | let imageView = LiftImageView()
114 | let analyzer = ImageAnalyzer()
115 | let interaction = ImageAnalysisInteraction()
116 |
117 | func makeUIView(context: Context) -> some UIView {
118 | imageView.image = UIImage(named: imageName)
119 | imageView.contentMode = .scaleAspectFit
120 | imageView.addInteraction(interaction)
121 | return imageView
122 | }
123 |
124 | func updateUIView(_ uiView: UIViewType, context: Context) {
125 |
126 | }
127 | }
128 | ```
129 |
130 | `imageName` here is a String type value, so you’d better prepare an image and add it to `Assets`. I named this image `dog.png` .
131 |
132 | `imageView` is a `LiftImageView` type value inherited from `UIImageView` . `LiftImageView` only use for resizing the image when embedding a `UIImageView` inside a SwiftUI view.
133 |
134 | ```swift
135 | class LiftImageView: UIImageView {
136 | // Use intrinsicContentSize to change the default image size
137 | // so that we can change the size in our SwiftUI View
138 | override var intrinsicContentSize: CGSize {
139 | .zero
140 | }
141 | }
142 | ```
143 |
144 | For iOS apps, you add the Live Text interface by adding an interaction object to the view containing the image. Add an `ImageAnalysisInteraction` object to the view’s interactions.
145 |
146 | ## Find items and start the interaction with an image
147 |
148 | `ImageAnalyzer.Configuration` object is used for specifying the types of items in the image we want to find. In this case, we want to consider all types of items. The initializing of `ImageAnalyzer.Configuration` object is easy:
149 |
150 | ```swift
151 | func updateUIView(_ uiView: UIViewType, context: Context) {
152 | Task {
153 | if let image = imageView.image {
154 | // Here I set configuration to contian all the configurations.
155 | let configuration = ImageAnalyzer.Configuration([.text, .visualLookUp, .machineReadableCode])
156 | }
157 | }
158 | }
159 | ```
160 |
161 | Then analyze the image by sending `analyze(_:configuration:)` to an `ImageAnalyzer` object, passing the image and the configuration. To improve performance, use a single shared instance of the analyzer throughout the app.
162 |
163 | ```swift
164 | func updateUIView(_ uiView: UIViewType, context: Context) {
165 | Task {
166 | if let image = imageView.image {
167 | // Here I set configuration to contian all the configurations.
168 | let configuration = ImageAnalyzer.Configuration([.text, .visualLookUp, .machineReadableCode])
169 | let analysis = try? await analyzer.analyze(image, configuration: configuration)
170 | }
171 | }
172 | }
173 | ```
174 |
175 | For iOS apps, start the image interface by setting the `analysis` property of the `ImageAnalysisInteraction` object to the results of the analyze method. For example, set the `analysis` property in the action method of a control that starts user interact with an image.
176 |
177 | ```swift
178 | func updateUIView(_ uiView: UIViewType, context: Context) {
179 | Task {
180 | if let image = imageView.image {
181 | // Here I set configuration to contian all the configurations.
182 | let configuration = ImageAnalyzer.Configuration([.text, .visualLookUp, .machineReadableCode])
183 | let analysis = try? await analyzer.analyze(image, configuration: configuration)
184 | if let analysis = analysis {
185 | interaction.analysis = analysis
186 | }
187 | }
188 | }
189 | }
190 | ```
191 |
192 | ## Customize the interface using interaction types
193 |
194 | You can change the behavior of the interface by enabling types of interactions with items found in the image. If you set the interaction or overlay view `preferredInteractionTypes` property to `automatic`, users can interact with all types of items that the analyzer finds in an image. For the text items, you can change the `preferredInteractionTypes` to `textSelection` .
195 |
196 | ```swift
197 | func updateUIView(_ uiView: UIViewType, context: Context) {
198 | Task {
199 | if let image = imageView.image {
200 | // Here I set configuration to contian all the configurations.
201 | let configuration = ImageAnalyzer.Configuration([.text, .visualLookUp, .machineReadableCode])
202 | let analysis = try? await analyzer.analyze(image, configuration: configuration)
203 | if let analysis = analysis {
204 | interaction.analysis = analysis
205 | // If just want recognize subject
206 | // use .imageSubject instead.
207 | interaction.preferredInteractionTypes = .automatic
208 | }
209 | }
210 | }
211 | }
212 | ```
213 |
214 | Now run this project and enjoy yourself.
215 |
216 | ## Source Code
217 |
218 | You can find the source code on [Github](https://github.com/HuangRunHua/wwdc23-code-notes/tree/main/lift-subjects-from-images).
219 |
220 | ## Supports Me
221 |
222 | If you think this article is helpful, you can support me by downloading my first Mac App which named [FilerApp](https://huangrunhua.github.io/FilerApp/) on the [Mac App Store](https://apps.apple.com/us/app/filerapp/id1626627609?mt=12&itsct=apps_box_link&itscg=30200). FilerApp is a Finder extension for your Mac which enables you to easily create files in supported formats anywhere on the system. It is free and useful for many people. Hope you like it.
223 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/lift-subjects-from-images/cover.png
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A99C11272A41DAEE0061DB01 /* lift_subjects_from_imagesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99C11262A41DAEE0061DB01 /* lift_subjects_from_imagesApp.swift */; };
11 | A99C11292A41DAEE0061DB01 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99C11282A41DAEE0061DB01 /* ContentView.swift */; };
12 | A99C112B2A41DAEF0061DB01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A99C112A2A41DAEF0061DB01 /* Assets.xcassets */; };
13 | A99C112E2A41DAEF0061DB01 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A99C112D2A41DAEF0061DB01 /* Preview Assets.xcassets */; };
14 | A99C11352A41DBD40061DB01 /* ImageLift.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99C11342A41DBD40061DB01 /* ImageLift.swift */; };
15 | A99C11372A41DE400061DB01 /* ImageLiftView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99C11362A41DE400061DB01 /* ImageLiftView.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | A99C11232A41DAEE0061DB01 /* lift-subjects-from-images.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "lift-subjects-from-images.app"; sourceTree = BUILT_PRODUCTS_DIR; };
20 | A99C11262A41DAEE0061DB01 /* lift_subjects_from_imagesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lift_subjects_from_imagesApp.swift; sourceTree = ""; };
21 | A99C11282A41DAEE0061DB01 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | A99C112A2A41DAEF0061DB01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | A99C112D2A41DAEF0061DB01 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | A99C11342A41DBD40061DB01 /* ImageLift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLift.swift; sourceTree = ""; };
25 | A99C11362A41DE400061DB01 /* ImageLiftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLiftView.swift; sourceTree = ""; };
26 | /* End PBXFileReference section */
27 |
28 | /* Begin PBXFrameworksBuildPhase section */
29 | A99C11202A41DAEE0061DB01 /* Frameworks */ = {
30 | isa = PBXFrameworksBuildPhase;
31 | buildActionMask = 2147483647;
32 | files = (
33 | );
34 | runOnlyForDeploymentPostprocessing = 0;
35 | };
36 | /* End PBXFrameworksBuildPhase section */
37 |
38 | /* Begin PBXGroup section */
39 | A99C111A2A41DAED0061DB01 = {
40 | isa = PBXGroup;
41 | children = (
42 | A99C11252A41DAEE0061DB01 /* lift-subjects-from-images */,
43 | A99C11242A41DAEE0061DB01 /* Products */,
44 | );
45 | sourceTree = "";
46 | };
47 | A99C11242A41DAEE0061DB01 /* Products */ = {
48 | isa = PBXGroup;
49 | children = (
50 | A99C11232A41DAEE0061DB01 /* lift-subjects-from-images.app */,
51 | );
52 | name = Products;
53 | sourceTree = "";
54 | };
55 | A99C11252A41DAEE0061DB01 /* lift-subjects-from-images */ = {
56 | isa = PBXGroup;
57 | children = (
58 | A99C11262A41DAEE0061DB01 /* lift_subjects_from_imagesApp.swift */,
59 | A99C11282A41DAEE0061DB01 /* ContentView.swift */,
60 | A99C11362A41DE400061DB01 /* ImageLiftView.swift */,
61 | A99C11342A41DBD40061DB01 /* ImageLift.swift */,
62 | A99C112A2A41DAEF0061DB01 /* Assets.xcassets */,
63 | A99C112C2A41DAEF0061DB01 /* Preview Content */,
64 | );
65 | path = "lift-subjects-from-images";
66 | sourceTree = "";
67 | };
68 | A99C112C2A41DAEF0061DB01 /* Preview Content */ = {
69 | isa = PBXGroup;
70 | children = (
71 | A99C112D2A41DAEF0061DB01 /* Preview Assets.xcassets */,
72 | );
73 | path = "Preview Content";
74 | sourceTree = "";
75 | };
76 | /* End PBXGroup section */
77 |
78 | /* Begin PBXNativeTarget section */
79 | A99C11222A41DAEE0061DB01 /* lift-subjects-from-images */ = {
80 | isa = PBXNativeTarget;
81 | buildConfigurationList = A99C11312A41DAEF0061DB01 /* Build configuration list for PBXNativeTarget "lift-subjects-from-images" */;
82 | buildPhases = (
83 | A99C111F2A41DAEE0061DB01 /* Sources */,
84 | A99C11202A41DAEE0061DB01 /* Frameworks */,
85 | A99C11212A41DAEE0061DB01 /* Resources */,
86 | );
87 | buildRules = (
88 | );
89 | dependencies = (
90 | );
91 | name = "lift-subjects-from-images";
92 | productName = "lift-subjects-from-images";
93 | productReference = A99C11232A41DAEE0061DB01 /* lift-subjects-from-images.app */;
94 | productType = "com.apple.product-type.application";
95 | };
96 | /* End PBXNativeTarget section */
97 |
98 | /* Begin PBXProject section */
99 | A99C111B2A41DAED0061DB01 /* Project object */ = {
100 | isa = PBXProject;
101 | attributes = {
102 | BuildIndependentTargetsInParallel = 1;
103 | LastSwiftUpdateCheck = 1500;
104 | LastUpgradeCheck = 1500;
105 | TargetAttributes = {
106 | A99C11222A41DAEE0061DB01 = {
107 | CreatedOnToolsVersion = 15.0;
108 | };
109 | };
110 | };
111 | buildConfigurationList = A99C111E2A41DAED0061DB01 /* Build configuration list for PBXProject "lift-subjects-from-images" */;
112 | compatibilityVersion = "Xcode 14.0";
113 | developmentRegion = en;
114 | hasScannedForEncodings = 0;
115 | knownRegions = (
116 | en,
117 | Base,
118 | );
119 | mainGroup = A99C111A2A41DAED0061DB01;
120 | productRefGroup = A99C11242A41DAEE0061DB01 /* Products */;
121 | projectDirPath = "";
122 | projectRoot = "";
123 | targets = (
124 | A99C11222A41DAEE0061DB01 /* lift-subjects-from-images */,
125 | );
126 | };
127 | /* End PBXProject section */
128 |
129 | /* Begin PBXResourcesBuildPhase section */
130 | A99C11212A41DAEE0061DB01 /* Resources */ = {
131 | isa = PBXResourcesBuildPhase;
132 | buildActionMask = 2147483647;
133 | files = (
134 | A99C112E2A41DAEF0061DB01 /* Preview Assets.xcassets in Resources */,
135 | A99C112B2A41DAEF0061DB01 /* Assets.xcassets in Resources */,
136 | );
137 | runOnlyForDeploymentPostprocessing = 0;
138 | };
139 | /* End PBXResourcesBuildPhase section */
140 |
141 | /* Begin PBXSourcesBuildPhase section */
142 | A99C111F2A41DAEE0061DB01 /* Sources */ = {
143 | isa = PBXSourcesBuildPhase;
144 | buildActionMask = 2147483647;
145 | files = (
146 | A99C11352A41DBD40061DB01 /* ImageLift.swift in Sources */,
147 | A99C11292A41DAEE0061DB01 /* ContentView.swift in Sources */,
148 | A99C11272A41DAEE0061DB01 /* lift_subjects_from_imagesApp.swift in Sources */,
149 | A99C11372A41DE400061DB01 /* ImageLiftView.swift in Sources */,
150 | );
151 | runOnlyForDeploymentPostprocessing = 0;
152 | };
153 | /* End PBXSourcesBuildPhase section */
154 |
155 | /* Begin XCBuildConfiguration section */
156 | A99C112F2A41DAEF0061DB01 /* Debug */ = {
157 | isa = XCBuildConfiguration;
158 | buildSettings = {
159 | ALWAYS_SEARCH_USER_PATHS = NO;
160 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
161 | CLANG_ANALYZER_NONNULL = YES;
162 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
163 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
164 | CLANG_ENABLE_MODULES = YES;
165 | CLANG_ENABLE_OBJC_ARC = YES;
166 | CLANG_ENABLE_OBJC_WEAK = YES;
167 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
168 | CLANG_WARN_BOOL_CONVERSION = YES;
169 | CLANG_WARN_COMMA = YES;
170 | CLANG_WARN_CONSTANT_CONVERSION = YES;
171 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
172 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
173 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
174 | CLANG_WARN_EMPTY_BODY = YES;
175 | CLANG_WARN_ENUM_CONVERSION = YES;
176 | CLANG_WARN_INFINITE_RECURSION = YES;
177 | CLANG_WARN_INT_CONVERSION = YES;
178 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
179 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
180 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
181 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
182 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
183 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
184 | CLANG_WARN_STRICT_PROTOTYPES = YES;
185 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
186 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
187 | CLANG_WARN_UNREACHABLE_CODE = YES;
188 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
189 | COPY_PHASE_STRIP = NO;
190 | DEBUG_INFORMATION_FORMAT = dwarf;
191 | ENABLE_STRICT_OBJC_MSGSEND = YES;
192 | ENABLE_TESTABILITY = YES;
193 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
194 | GCC_C_LANGUAGE_STANDARD = gnu17;
195 | GCC_DYNAMIC_NO_PIC = NO;
196 | GCC_NO_COMMON_BLOCKS = YES;
197 | GCC_OPTIMIZATION_LEVEL = 0;
198 | GCC_PREPROCESSOR_DEFINITIONS = (
199 | "DEBUG=1",
200 | "$(inherited)",
201 | );
202 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
203 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
204 | GCC_WARN_UNDECLARED_SELECTOR = YES;
205 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
206 | GCC_WARN_UNUSED_FUNCTION = YES;
207 | GCC_WARN_UNUSED_VARIABLE = YES;
208 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
209 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
210 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
211 | MTL_FAST_MATH = YES;
212 | ONLY_ACTIVE_ARCH = YES;
213 | SDKROOT = iphoneos;
214 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
215 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
216 | };
217 | name = Debug;
218 | };
219 | A99C11302A41DAEF0061DB01 /* Release */ = {
220 | isa = XCBuildConfiguration;
221 | buildSettings = {
222 | ALWAYS_SEARCH_USER_PATHS = NO;
223 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
224 | CLANG_ANALYZER_NONNULL = YES;
225 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
227 | CLANG_ENABLE_MODULES = YES;
228 | CLANG_ENABLE_OBJC_ARC = YES;
229 | CLANG_ENABLE_OBJC_WEAK = YES;
230 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
231 | CLANG_WARN_BOOL_CONVERSION = YES;
232 | CLANG_WARN_COMMA = YES;
233 | CLANG_WARN_CONSTANT_CONVERSION = YES;
234 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
235 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
236 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
237 | CLANG_WARN_EMPTY_BODY = YES;
238 | CLANG_WARN_ENUM_CONVERSION = YES;
239 | CLANG_WARN_INFINITE_RECURSION = YES;
240 | CLANG_WARN_INT_CONVERSION = YES;
241 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
242 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
243 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
244 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
245 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
246 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
247 | CLANG_WARN_STRICT_PROTOTYPES = YES;
248 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
249 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
250 | CLANG_WARN_UNREACHABLE_CODE = YES;
251 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
252 | COPY_PHASE_STRIP = NO;
253 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
254 | ENABLE_NS_ASSERTIONS = NO;
255 | ENABLE_STRICT_OBJC_MSGSEND = YES;
256 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
257 | GCC_C_LANGUAGE_STANDARD = gnu17;
258 | GCC_NO_COMMON_BLOCKS = YES;
259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
261 | GCC_WARN_UNDECLARED_SELECTOR = YES;
262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
263 | GCC_WARN_UNUSED_FUNCTION = YES;
264 | GCC_WARN_UNUSED_VARIABLE = YES;
265 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
266 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
267 | MTL_ENABLE_DEBUG_INFO = NO;
268 | MTL_FAST_MATH = YES;
269 | SDKROOT = iphoneos;
270 | SWIFT_COMPILATION_MODE = wholemodule;
271 | VALIDATE_PRODUCT = YES;
272 | };
273 | name = Release;
274 | };
275 | A99C11322A41DAEF0061DB01 /* Debug */ = {
276 | isa = XCBuildConfiguration;
277 | buildSettings = {
278 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
279 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
280 | CODE_SIGN_STYLE = Automatic;
281 | CURRENT_PROJECT_VERSION = 1;
282 | DEVELOPMENT_ASSET_PATHS = "\"lift-subjects-from-images/Preview Content\"";
283 | DEVELOPMENT_TEAM = YRB62S584T;
284 | ENABLE_PREVIEWS = YES;
285 | GENERATE_INFOPLIST_FILE = YES;
286 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
287 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
288 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
289 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
290 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
291 | LD_RUNPATH_SEARCH_PATHS = (
292 | "$(inherited)",
293 | "@executable_path/Frameworks",
294 | );
295 | MARKETING_VERSION = 1.0;
296 | PRODUCT_BUNDLE_IDENTIFIER = "com.noreply-eamil.lift-subjects-from-images";
297 | PRODUCT_NAME = "$(TARGET_NAME)";
298 | SWIFT_EMIT_LOC_STRINGS = YES;
299 | SWIFT_VERSION = 5.0;
300 | TARGETED_DEVICE_FAMILY = "1,2";
301 | };
302 | name = Debug;
303 | };
304 | A99C11332A41DAEF0061DB01 /* Release */ = {
305 | isa = XCBuildConfiguration;
306 | buildSettings = {
307 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
308 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
309 | CODE_SIGN_STYLE = Automatic;
310 | CURRENT_PROJECT_VERSION = 1;
311 | DEVELOPMENT_ASSET_PATHS = "\"lift-subjects-from-images/Preview Content\"";
312 | DEVELOPMENT_TEAM = YRB62S584T;
313 | ENABLE_PREVIEWS = YES;
314 | GENERATE_INFOPLIST_FILE = YES;
315 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
316 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
317 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
318 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
319 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
320 | LD_RUNPATH_SEARCH_PATHS = (
321 | "$(inherited)",
322 | "@executable_path/Frameworks",
323 | );
324 | MARKETING_VERSION = 1.0;
325 | PRODUCT_BUNDLE_IDENTIFIER = "com.noreply-eamil.lift-subjects-from-images";
326 | PRODUCT_NAME = "$(TARGET_NAME)";
327 | SWIFT_EMIT_LOC_STRINGS = YES;
328 | SWIFT_VERSION = 5.0;
329 | TARGETED_DEVICE_FAMILY = "1,2";
330 | };
331 | name = Release;
332 | };
333 | /* End XCBuildConfiguration section */
334 |
335 | /* Begin XCConfigurationList section */
336 | A99C111E2A41DAED0061DB01 /* Build configuration list for PBXProject "lift-subjects-from-images" */ = {
337 | isa = XCConfigurationList;
338 | buildConfigurations = (
339 | A99C112F2A41DAEF0061DB01 /* Debug */,
340 | A99C11302A41DAEF0061DB01 /* Release */,
341 | );
342 | defaultConfigurationIsVisible = 0;
343 | defaultConfigurationName = Release;
344 | };
345 | A99C11312A41DAEF0061DB01 /* Build configuration list for PBXNativeTarget "lift-subjects-from-images" */ = {
346 | isa = XCConfigurationList;
347 | buildConfigurations = (
348 | A99C11322A41DAEF0061DB01 /* Debug */,
349 | A99C11332A41DAEF0061DB01 /* Release */,
350 | );
351 | defaultConfigurationIsVisible = 0;
352 | defaultConfigurationName = Release;
353 | };
354 | /* End XCConfigurationList section */
355 | };
356 | rootObject = A99C111B2A41DAED0061DB01 /* Project object */;
357 | }
358 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/lift-subjects-from-images/lift-subjects-from-images.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images.xcodeproj/xcuserdata/huangrunhua.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | lift-subjects-from-images.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/cat.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "FqdQZsHWAAA5AzC.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/cat.imageset/FqdQZsHWAAA5AzC.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/cat.imageset/FqdQZsHWAAA5AzC.jpg
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/dog.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "dog.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/dog.imageset/dog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/lift-subjects-from-images/lift-subjects-from-images/Assets.xcassets/dog.imageset/dog.png
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // lift-subjects-from-images
4 | //
5 | // Created by Huang Runhua on 6/20/23.
6 | //
7 |
8 | import SwiftUI
9 | import VisionKit
10 |
11 | struct ContentView: View {
12 | @State private var deviceSupportLiftSubject = false
13 | @State private var showDeviceNotCapacityAlert = false
14 | @State private var showImageLiftView = false
15 | var body: some View {
16 | Button {
17 | if deviceSupportLiftSubject {
18 | self.showImageLiftView = true
19 | } else {
20 | self.showDeviceNotCapacityAlert = true
21 | }
22 | } label: {
23 | Text("Pick an Image")
24 | .foregroundColor(.white)
25 | .padding()
26 | .frame(width: 300, height: 50)
27 | .background(Color.blue)
28 | .cornerRadius(10)
29 | }
30 | .alert("Lift Subject Unavailable", isPresented: $showDeviceNotCapacityAlert, actions: {})
31 | .sheet(isPresented: $showImageLiftView, content: {
32 | ImageLiftView()
33 | })
34 | .onAppear {
35 | self.deviceSupportLiftSubject = ImageAnalyzer.isSupported
36 | }
37 | }
38 | }
39 |
40 | #Preview {
41 | ContentView()
42 | }
43 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/ImageLift.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageLift.swift
3 | // lift-subjects-from-images
4 | //
5 | // Created by Huang Runhua on 6/20/23.
6 | //
7 |
8 | import SwiftUI
9 | import VisionKit
10 |
11 | @MainActor
12 | struct ImageLift: UIViewRepresentable {
13 | var imageName: String
14 | let imageView = LiftImageView()
15 | let analyzer = ImageAnalyzer()
16 | let interaction = ImageAnalysisInteraction()
17 |
18 | func makeUIView(context: Context) -> some UIView {
19 | imageView.image = UIImage(named: imageName)
20 | imageView.contentMode = .scaleAspectFit
21 | imageView.addInteraction(interaction)
22 | return imageView
23 | }
24 |
25 | func updateUIView(_ uiView: UIViewType, context: Context) {
26 | Task {
27 | if let image = imageView.image {
28 | // Here I set configuration to contian all the configurations.
29 | let configuration = ImageAnalyzer.Configuration([.text, .visualLookUp, .machineReadableCode])
30 | let analysis = try? await analyzer.analyze(image, configuration: configuration)
31 | if let analysis = analysis {
32 | interaction.analysis = analysis
33 | // If just want recognize subject
34 | // use .imageSubject instead.
35 | interaction.preferredInteractionTypes = .automatic
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 |
43 | class LiftImageView: UIImageView {
44 | // Use intrinsicContentSize to change the default image size
45 | // so that we can change the size in our SwiftUI View
46 | override var intrinsicContentSize: CGSize {
47 | .zero
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/ImageLiftView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageLiftView.swift
3 | // lift-subjects-from-images
4 | //
5 | // Created by Huang Runhua on 6/20/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ImageLiftView: View {
11 | @Environment(\.presentationMode) var presentationMode
12 | var body: some View {
13 | NavigationView {
14 | ImageLift(imageName: "dog")
15 | .toolbar {
16 | ToolbarItem(placement: .navigationBarLeading) {
17 | Button {
18 | self.presentationMode.wrappedValue.dismiss()
19 | } label: {
20 | Text("Cancel")
21 | }
22 | }
23 | }
24 | .interactiveDismissDisabled(true)
25 | }
26 | }
27 | }
28 |
29 | #Preview {
30 | ImageLiftView()
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/lift-subjects-from-images/lift-subjects-from-images/lift_subjects_from_imagesApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // lift_subjects_from_imagesApp.swift
3 | // lift-subjects-from-images
4 | //
5 | // Created by Huang Runhua on 6/20/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct lift_subjects_from_imagesApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/.DS_Store
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/.DS_Store
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/1.png
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/2.png
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/3.png
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/4.png
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/IMG_7754.JPEG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/IMG_7754.JPEG
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/article-images/IMG_7762.JPEG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/article-images/IMG_7762.JPEG
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/xcshareddata/xcschemes/meet-storekit-for-swiftui.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
54 |
55 |
56 |
62 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui.xcodeproj/xcuserdata/huangrunhua.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | meet-storekit-for-swiftui.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | A9EFF4A72A343D8E00F20785
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Cold Winter.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "IMG_7731.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Cold Winter.imageset/IMG_7731.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Cold Winter.imageset/IMG_7731.jpg
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "properties" : {
7 | "provides-namespace" : true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Drunk.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "IMG_7729.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Drunk.imageset/IMG_7729.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Drunk.imageset/IMG_7729.jpg
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Platinum Disco.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "IMG_7730.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Platinum Disco.imageset/IMG_7730.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Assets.xcassets/Musics/Platinum Disco.imageset/IMG_7730.jpg
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | @State private var showShopStore: Bool = false
13 |
14 | var storeModel: StoreModel
15 |
16 | var body: some View {
17 | NavigationView {
18 | VStack {
19 | if storeModel.ownedSongProducts.isEmpty {
20 | Text("Empty Library")
21 | .font(.title)
22 | .foregroundStyle(.gray)
23 | } else {
24 | ScrollView {
25 | ForEach(storeModel.ownedSongProducts) { song in
26 | SongCellView(music: song)
27 | .padding([.leading, .trailing])
28 | }
29 | }
30 | }
31 | }
32 | .navigationTitle("Songs")
33 | .toolbar(content: {
34 | ToolbarItem {
35 | Button(action: {
36 | self.showShopStore = true
37 | }, label: {
38 | Label("Shop Store", systemImage: "cart")
39 | })
40 | }
41 | })
42 | }
43 | .sheet(isPresented: $showShopStore, content: {
44 | SongProductShop()
45 | })
46 | .onAppear(perform: {
47 | SongProductPurchase.createSharedInstance(storeModel: storeModel)
48 | })
49 | .onInAppPurchaseCompletion { product, purchaseResult in
50 | if case .success(.success(let transaction)) = purchaseResult {
51 | await SongProductPurchase.shared.process(transaction: transaction)
52 | }
53 | self.showShopStore = false
54 | }
55 | .task {
56 | // Begin observing StoreKit transaction updates in case a
57 | // transaction happens on another device.
58 | await SongProductPurchase.shared.observeTransactionUpdates()
59 | // Check if we have any unfinished transactions where we
60 | // need to grant access to content
61 | await SongProductPurchase.shared.checkForUnfinishedTransactions()
62 | }
63 | }
64 | }
65 |
66 | #Preview {
67 | ContentView(storeModel: StoreModel())
68 | }
69 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Misc/Store.storekit:
--------------------------------------------------------------------------------
1 | {
2 | "identifier" : "E6C6C140",
3 | "nonRenewingSubscriptions" : [
4 |
5 | ],
6 | "products" : [
7 | {
8 | "displayPrice" : "2.99",
9 | "familyShareable" : false,
10 | "internalID" : "4A77BC7F",
11 | "localizations" : [
12 | {
13 | "description" : "Hip-Hop/Rap 2020",
14 | "displayName" : "Cold Winter",
15 | "locale" : "en_US"
16 | }
17 | ],
18 | "productID" : "com.meet.storekit.for.swiftui.cold.winter",
19 | "referenceName" : "Cold Winter",
20 | "type" : "NonConsumable"
21 | },
22 | {
23 | "displayPrice" : "2.99",
24 | "familyShareable" : false,
25 | "internalID" : "2B04B480",
26 | "localizations" : [
27 | {
28 | "description" : "J-Pop 2014, Lossless",
29 | "displayName" : "Platinum Disco",
30 | "locale" : "en_US"
31 | }
32 | ],
33 | "productID" : "com.meet.storekit.for.swiftui.platinum.disco",
34 | "referenceName" : "Platinum Disco",
35 | "type" : "NonConsumable"
36 | },
37 | {
38 | "displayPrice" : "0.99",
39 | "familyShareable" : false,
40 | "internalID" : "6C7321E2",
41 | "localizations" : [
42 | {
43 | "description" : "International Pop 2015, Lossless",
44 | "displayName" : "Drunk",
45 | "locale" : "en_US"
46 | }
47 | ],
48 | "productID" : "com.meet.storekit.for.swiftui.drunk",
49 | "referenceName" : "Drunk",
50 | "type" : "NonConsumable"
51 | }
52 | ],
53 | "settings" : {
54 | "_failTransactionsEnabled" : false,
55 | "_locale" : "en_US",
56 | "_storefront" : "USA",
57 | "_storeKitErrors" : [
58 | {
59 | "current" : {
60 | "index" : 2,
61 | "type" : "generic"
62 | },
63 | "enabled" : false,
64 | "name" : "Load Products"
65 | },
66 | {
67 | "current" : null,
68 | "enabled" : false,
69 | "name" : "Purchase"
70 | },
71 | {
72 | "current" : null,
73 | "enabled" : false,
74 | "name" : "Verification"
75 | },
76 | {
77 | "current" : null,
78 | "enabled" : false,
79 | "name" : "App Store Sync"
80 | },
81 | {
82 | "current" : null,
83 | "enabled" : false,
84 | "name" : "Subscription Status"
85 | },
86 | {
87 | "current" : null,
88 | "enabled" : false,
89 | "name" : "App Transaction"
90 | },
91 | {
92 | "current" : null,
93 | "enabled" : false,
94 | "name" : "Manage Subscriptions Sheet"
95 | },
96 | {
97 | "current" : null,
98 | "enabled" : false,
99 | "name" : "Refund Request Sheet"
100 | },
101 | {
102 | "current" : null,
103 | "enabled" : false,
104 | "name" : "Offer Code Redeem Sheet"
105 | }
106 | ]
107 | },
108 | "subscriptionGroups" : [
109 |
110 | ],
111 | "version" : {
112 | "major" : 3,
113 | "minor" : 0
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Model/SongProduct+DataGeneration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongProduct+DataGeneration.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SongProduct {
11 | static let allSongProducts: [SongProduct] = [
12 | SongProduct(id: 0,
13 | productID: "com.meet.storekit.for.swiftui.cold.winter",
14 | name: "Cold Winter",
15 | summary: "Hip-Hop/Rap 2020",
16 | isPurchased: false),
17 | SongProduct(id: 1,
18 | productID: "com.meet.storekit.for.swiftui.platinum.disco",
19 | name: "Platinum Disco",
20 | summary: "J-Pop 2014, Lossless",
21 | isPurchased: false),
22 | SongProduct(id: 2,
23 | productID: "com.meet.storekit.for.swiftui.drunk",
24 | name: "Drunk",
25 | summary: "International Pop 2015, Lossless",
26 | isPurchased: false)
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Model/SongProduct.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongProduct.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | class SongProduct: Identifiable {
11 | public var id: Int
12 | public var productID: String
13 | public var name: String
14 | public var summary: String
15 | public var isPurchased: Bool
16 |
17 | var image: Image {
18 | Image("Musics/\(name)")
19 | .resizable()
20 | }
21 |
22 | public init(
23 | id: Int,
24 | productID: String,
25 | name: String,
26 | summary: String,
27 | isPurchased: Bool
28 | ) {
29 | self.id = id
30 | self.productID = productID
31 | self.name = name
32 | self.summary = summary
33 | self.isPurchased = isPurchased
34 | }
35 | }
36 |
37 | extension Sequence where Element == SongProduct {
38 | func song(for productID: String) -> SongProduct? {
39 | lazy.first(where: { $0.productID == productID })
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Model/SongProductPurchase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongProductPurchase.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import Foundation
9 | import StoreKit
10 |
11 | actor SongProductPurchase {
12 |
13 | var storeModel: StoreModel
14 |
15 | private init(storeModel: StoreModel) {
16 | self.storeModel = storeModel
17 | }
18 |
19 | private(set) static var shared: SongProductPurchase!
20 |
21 | static func createSharedInstance(storeModel: StoreModel) {
22 | shared = SongProductPurchase(storeModel: storeModel)
23 | }
24 |
25 | func process(transaction verificationResult: VerificationResult) async {
26 | let transaction: Transaction
27 | switch verificationResult {
28 | case .verified(let t):
29 | transaction = t
30 | case .unverified(_, let error):
31 | print("error in process: \(error.localizedDescription)")
32 | return
33 | }
34 |
35 | if case .nonConsumable = transaction.productType {
36 | guard let songProduct = song(for: transaction.productID) else {
37 | return
38 | }
39 | if transaction.revocationDate == nil, transaction.revocationReason == nil {
40 | songProduct.isPurchased = true
41 | } else {
42 | songProduct.isPurchased = false
43 | }
44 | } else {
45 | await transaction.finish()
46 | }
47 |
48 | storeModel.ownedSongProducts = SongProduct.allSongProducts.filter({ $0.isPurchased })
49 | }
50 |
51 | func checkForUnfinishedTransactions() async {
52 | for await transaction in Transaction.unfinished {
53 | Task.detached(priority: .background) {
54 | await self.process(transaction: transaction)
55 | }
56 | }
57 | }
58 |
59 | func observeTransactionUpdates() async {
60 | for await update in Transaction.updates {
61 | await self.process(transaction: update)
62 | }
63 | }
64 |
65 | private func song(for productID: Product.ID) -> SongProduct? {
66 | SongProduct.allSongProducts.song(for: productID)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Model/StoreModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoreModel.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import Foundation
9 | import Observation
10 |
11 | @Observable public class StoreModel {
12 | var ownedSongProducts: [SongProduct] = []
13 | }
14 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Views/SongCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicCellView.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SongCellView: View {
11 | var music: SongProduct
12 | var body: some View {
13 | VStack {
14 | HStack {
15 | music.image
16 | .resizable()
17 | .aspectRatio(contentMode: .fit)
18 | .frame(width: 50, height: 50)
19 | .clipShape(RoundedRectangle(cornerRadius: 5))
20 | VStack(alignment: .leading, spacing: 3) {
21 | Text(music.name)
22 | .font(.system(size: 17))
23 | Text(music.summary)
24 | .foregroundStyle(.gray)
25 | .font(.system(size: 15))
26 | }
27 | .frame(height: 50)
28 | Spacer()
29 | }
30 | Divider()
31 | }
32 | }
33 | }
34 |
35 | #Preview {
36 | SongCellView(music: SongProduct.allSongProducts[0])
37 | }
38 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Views/SongProductProductIcon.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongProductProductIcon.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SongProductProductIcon: View {
11 |
12 | var productID: String
13 | var song: SongProduct? {
14 | SongProduct.allSongProducts.song(for: productID)
15 | }
16 |
17 | var body: some View {
18 | if let song {
19 | song.image
20 | .scaledToFit()
21 | .clipShape(RoundedRectangle(cornerRadius: 7))
22 | } else {
23 | EmptyView()
24 | }
25 | }
26 | }
27 |
28 | #Preview {
29 | SongProductProductIcon(productID: "com.meet.storekit.for.swiftui.cold.winter")
30 | }
31 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/Views/SongProductShop.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongProductShop.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import SwiftUI
9 | import StoreKit
10 |
11 | struct SongProductShop: View {
12 | private var musics: [SongProduct] {
13 | SongProduct.allSongProducts
14 | }
15 |
16 | @State private var isRestoring = false
17 |
18 | var body: some View {
19 | NavigationView {
20 | StoreView(ids: productIDs) { product in
21 | SongProductProductIcon(productID: product.id)
22 | }
23 | .navigationTitle("Song Shop")
24 | .storeButton(.hidden, for: .cancellation)
25 | .productViewStyle(.regular)
26 | .toolbar {
27 | ToolbarItem {
28 | Button("Restore") {
29 | isRestoring = true
30 | Task.detached {
31 | defer { isRestoring = false }
32 | try await AppStore.sync()
33 | }
34 | }
35 | .disabled(isRestoring)
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 | #Preview {
43 | SongProductShop()
44 | }
45 |
46 | extension SongProductShop {
47 | private var productIDs: some Collection {
48 | musics.lazy
49 | .map(\.productID)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/meet-storekit-for-swiftui/meet-storekit-for-swiftui/meet_storekit_for_swiftuiApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // meet_storekit_for_swiftuiApp.swift
3 | // meet-storekit-for-swiftui
4 | //
5 | // Created by Huang Runhua on 6/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct meet_storekit_for_swiftuiApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView(storeModel: StoreModel())
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/.DS_Store
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/1.JPEG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/1.JPEG
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/2.JPEG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/2.JPEG
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/3.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/4.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/5.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/6.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/7.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/article-images/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/article-images/8.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/project.xcworkspace/xcuserdata/runhuahuang.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/project.xcworkspace/xcuserdata/runhuahuang.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/xcshareddata/xcschemes/meet-subscriptionstoreview-in-iOS17.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
54 |
55 |
56 |
62 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17.xcodeproj/xcuserdata/huangrunhua.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | meet-subscriptionstoreview-in-iOS17.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | A963C56B2A39E839005F2BB2
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/.DS_Store
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/AppIcon.appiconset/1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/AppIcon.appiconset/1-1.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "1-1.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/movie.imageset/1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/movie.imageset/1-1.png
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Assets.xcassets/movie.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "1-1.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/14/23.
6 | //
7 |
8 | import SwiftUI
9 | import StoreKit
10 |
11 | struct ContentView: View {
12 |
13 | @State private var showScriptionView: Bool = false
14 | @State private var status: EntitlementTaskState = .loading
15 | @State private var presentingSubscriptionSheet = false
16 |
17 | @Environment(PassStatusModel.self) var passStatusModel: PassStatusModel
18 | @Environment(\.passIDs) private var passIDs
19 |
20 |
21 | var body: some View {
22 | NavigationView {
23 | List {
24 | Section {
25 | planView
26 | // Show the option button if user does not have a plan.
27 | if passStatusModel.passStatus == .notSubscribed {
28 | Button {
29 | self.showScriptionView = true
30 | } label: {
31 | Text("View Options")
32 | }
33 | }
34 | } header: {
35 | Text("SUBSCRIPTION")
36 | } footer: {
37 | if passStatusModel.passStatus != .notSubscribed {
38 | Text("Flower Movie+ Plan: \(String(describing: passStatusModel.passStatus.description))")
39 | }
40 | }
41 | }
42 | .navigationTitle("Account")
43 | .sheet(isPresented: $showScriptionView, content: {
44 | SubscriptionShopView()
45 | })
46 |
47 | }
48 | .manageSubscriptionsSheet(
49 | isPresented: $presentingSubscriptionSheet,
50 | subscriptionGroupID: passIDs.group
51 | )
52 | .onAppear(perform: {
53 | ProductSubscription.createSharedInstance()
54 | })
55 | .subscriptionStatusTask(for: passIDs.group) { taskStatus in
56 | self.status = await taskStatus.map { statuses in
57 | await ProductSubscription.shared.status(
58 | for: statuses,
59 | ids: passIDs
60 | )
61 | }
62 | switch self.status {
63 | case .failure(let error):
64 | passStatusModel.passStatus = .notSubscribed
65 | print("Failed to check subscription status: \(error)")
66 | case .success(let status):
67 | passStatusModel.passStatus = status
68 | case .loading: break
69 | @unknown default: break
70 | }
71 | }
72 | .task {
73 | await ProductSubscription.shared.observeTransactionUpdates()
74 | await ProductSubscription.shared.checkForUnfinishedTransactions()
75 | }
76 | }
77 | }
78 |
79 | #Preview {
80 | ContentView()
81 | .environment(PassStatusModel())
82 | }
83 |
84 |
85 | extension ContentView {
86 | @ViewBuilder
87 | var planView: some View {
88 | VStack(alignment: .leading, spacing: 3) {
89 | Text(passStatusModel.passStatus == .notSubscribed ? "Flower Movie+": "Flower Movie+ Plan: \(passStatusModel.passStatus.description)")
90 | .font(.system(size: 17))
91 | Text(passStatusModel.passStatus == .notSubscribed ? "Subscription to unlock all streaming videos, enjoy Blu-ray 4K quality, and watch offline.": "Enjoy all streaming Blu-ray 4K quality videos, and watch offline.")
92 | .font(.system(size: 15))
93 | .foregroundStyle(.gray)
94 | if passStatusModel.passStatus != .notSubscribed {
95 | Button("Handle Subscription \(Image(systemName: "chevron.forward"))") {
96 | self.presentingSubscriptionSheet = true
97 | }
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Misc/Store.storekit:
--------------------------------------------------------------------------------
1 | {
2 | "identifier" : "D1189687",
3 | "nonRenewingSubscriptions" : [
4 |
5 | ],
6 | "products" : [
7 |
8 | ],
9 | "settings" : {
10 | "_compatibilityTimeRate" : {
11 | "3" : 6
12 | },
13 | "_failTransactionsEnabled" : false,
14 | "_locale" : "en_US",
15 | "_storefront" : "USA",
16 | "_storeKitErrors" : [
17 | {
18 | "current" : {
19 | "index" : 2,
20 | "type" : "generic"
21 | },
22 | "enabled" : false,
23 | "name" : "Load Products"
24 | },
25 | {
26 | "current" : {
27 | "index" : 1,
28 | "type" : "purchase"
29 | },
30 | "enabled" : false,
31 | "name" : "Purchase"
32 | },
33 | {
34 | "current" : null,
35 | "enabled" : false,
36 | "name" : "Verification"
37 | },
38 | {
39 | "current" : null,
40 | "enabled" : false,
41 | "name" : "App Store Sync"
42 | },
43 | {
44 | "current" : {
45 | "index" : 0,
46 | "type" : "generic"
47 | },
48 | "enabled" : false,
49 | "name" : "Subscription Status"
50 | },
51 | {
52 | "current" : null,
53 | "enabled" : false,
54 | "name" : "App Transaction"
55 | },
56 | {
57 | "current" : null,
58 | "enabled" : false,
59 | "name" : "Manage Subscriptions Sheet"
60 | },
61 | {
62 | "current" : null,
63 | "enabled" : false,
64 | "name" : "Refund Request Sheet"
65 | },
66 | {
67 | "current" : null,
68 | "enabled" : false,
69 | "name" : "Offer Code Redeem Sheet"
70 | }
71 | ],
72 | "_timeRate" : 1004
73 | },
74 | "subscriptionGroups" : [
75 | {
76 | "id" : "506F71A6",
77 | "localizations" : [
78 |
79 | ],
80 | "name" : "Flower Movie+",
81 | "subscriptions" : [
82 | {
83 | "adHocOffers" : [
84 |
85 | ],
86 | "codeOffers" : [
87 |
88 | ],
89 | "displayPrice" : "4.99",
90 | "familyShareable" : false,
91 | "groupNumber" : 1,
92 | "internalID" : "67522F32",
93 | "introductoryOffer" : {
94 | "displayPrice" : "0.99",
95 | "internalID" : "C032836B",
96 | "paymentMode" : "free",
97 | "subscriptionPeriod" : "P3D"
98 | },
99 | "localizations" : [
100 | {
101 | "description" : "Unlock all streaming videos",
102 | "displayName" : "Monthly",
103 | "locale" : "en_US"
104 | }
105 | ],
106 | "productID" : "com.pass.monthly",
107 | "recurringSubscriptionPeriod" : "P1M",
108 | "referenceName" : "Monthly",
109 | "subscriptionGroupID" : "506F71A6",
110 | "type" : "RecurringSubscription"
111 | },
112 | {
113 | "adHocOffers" : [
114 |
115 | ],
116 | "codeOffers" : [
117 |
118 | ],
119 | "displayPrice" : "12.99",
120 | "familyShareable" : false,
121 | "groupNumber" : 1,
122 | "internalID" : "22A924F3",
123 | "introductoryOffer" : {
124 | "internalID" : "543C4327",
125 | "paymentMode" : "free",
126 | "subscriptionPeriod" : "P3D"
127 | },
128 | "localizations" : [
129 | {
130 | "description" : "Unlock all streaming videos",
131 | "displayName" : "Quarterly",
132 | "locale" : "en_US"
133 | }
134 | ],
135 | "productID" : "com.pass.quarterly",
136 | "recurringSubscriptionPeriod" : "P3M",
137 | "referenceName" : "Quarterly",
138 | "subscriptionGroupID" : "506F71A6",
139 | "type" : "RecurringSubscription"
140 | },
141 | {
142 | "adHocOffers" : [
143 |
144 | ],
145 | "codeOffers" : [
146 |
147 | ],
148 | "displayPrice" : "39.99",
149 | "familyShareable" : false,
150 | "groupNumber" : 1,
151 | "internalID" : "469A83B4",
152 | "introductoryOffer" : {
153 | "displayPrice" : "0.99",
154 | "internalID" : "0B3182A5",
155 | "paymentMode" : "free",
156 | "subscriptionPeriod" : "P3D"
157 | },
158 | "localizations" : [
159 | {
160 | "description" : "Unlock all streaming videos",
161 | "displayName" : "Yearly",
162 | "locale" : "en_US"
163 | }
164 | ],
165 | "productID" : "com.pass.yearly",
166 | "recurringSubscriptionPeriod" : "P1Y",
167 | "referenceName" : "Yearly",
168 | "subscriptionGroupID" : "506F71A6",
169 | "type" : "RecurringSubscription"
170 | }
171 | ]
172 | }
173 | ],
174 | "version" : {
175 | "major" : 3,
176 | "minor" : 0
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Model/PassStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PassStatus.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/14/23.
6 | //
7 |
8 | import Foundation
9 | import StoreKit
10 |
11 | enum PassStatus: Comparable, Hashable {
12 | case notSubscribed
13 | case monthly
14 | case quarterly
15 | case yearly
16 |
17 | init?(productID: Product.ID, ids: PassIdentifiers) {
18 | switch productID {
19 | case ids.monthly: self = .monthly
20 | case ids.quarterly: self = .quarterly
21 | case ids.yearly: self = .yearly
22 | default: return nil
23 | }
24 | }
25 |
26 | var description: String {
27 | switch self {
28 | case .notSubscribed:
29 | "Not Subscribed"
30 | case .monthly:
31 | "Monthly"
32 | case .quarterly:
33 | "Quarterly"
34 | case .yearly:
35 | "Yearly"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Model/PassStatusModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PassStatusModel.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/14/23.
6 | //
7 |
8 | import Foundation
9 | import Observation
10 |
11 | @Observable class PassStatusModel {
12 | var passStatus: PassStatus = .notSubscribed
13 | }
14 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Model/ProductSubscription.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProductSubscription.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/15/23.
6 | //
7 |
8 | import OSLog
9 | import Foundation
10 | import StoreKit
11 |
12 | actor ProductSubscription {
13 | private let logger = Logger(
14 | subsystem: "Meet SubscriptionView",
15 | category: "Product Subscription"
16 | )
17 |
18 | private init() {}
19 |
20 | private(set) static var shared: ProductSubscription!
21 |
22 | static func createSharedInstance() {
23 | shared = ProductSubscription()
24 | }
25 |
26 | // Subscription Only Handle Here.
27 | func status(for statuses: [Product.SubscriptionInfo.Status], ids: PassIdentifiers) -> PassStatus {
28 | let effectiveStatus = statuses.max { lhs, rhs in
29 | let lhsStatus = PassStatus(
30 | productID: lhs.transaction.unsafePayloadValue.productID,
31 | ids: ids
32 | ) ?? .notSubscribed
33 | let rhsStatus = PassStatus(
34 | productID: rhs.transaction.unsafePayloadValue.productID,
35 | ids: ids
36 | ) ?? .notSubscribed
37 | return lhsStatus < rhsStatus
38 | }
39 | guard let effectiveStatus else {
40 | return .notSubscribed
41 | }
42 |
43 | let transaction: Transaction
44 | switch effectiveStatus.transaction {
45 | case .verified(let t):
46 | transaction = t
47 | case .unverified(_, let error):
48 | print("Error occured in status(for:ids:): \(error)")
49 | return .notSubscribed
50 | }
51 |
52 | if case .autoRenewable = transaction.productType {
53 | if !(transaction.revocationDate == nil && transaction.revocationReason == nil) {
54 | return .notSubscribed
55 | }
56 | if let subscriptionExpirationDate = transaction.expirationDate {
57 | if subscriptionExpirationDate.timeIntervalSince1970 < Date().timeIntervalSince1970 {
58 | return .notSubscribed
59 | }
60 | }
61 | }
62 |
63 | return PassStatus(productID: transaction.productID, ids: ids) ?? .notSubscribed
64 | }
65 |
66 |
67 | }
68 |
69 | // To discard this warning:
70 | // Making a purchase without listening for
71 | // transaction updates risks missing successful purchases.
72 | // Create a Task to iterate Transaction.updates at launch.
73 | extension ProductSubscription {
74 | // For other in-app purchase use this method to check for status.
75 | func process(transaction verificationResult: VerificationResult) async {
76 | do {
77 | let unsafeTransaction = verificationResult.unsafePayloadValue
78 | logger.log("""
79 | Processing transaction ID \(unsafeTransaction.id) for \
80 | \(unsafeTransaction.productID)
81 | """)
82 | }
83 |
84 | let transaction: Transaction
85 | switch verificationResult {
86 | case .verified(let t):
87 | logger.debug("""
88 | Transaction ID \(t.id) for \(t.productID) is verified
89 | """)
90 | transaction = t
91 | case .unverified(let t, let error):
92 | // Log failure and ignore unverified transactions
93 | logger.error("""
94 | Transaction ID \(t.id) for \(t.productID) is unverified: \(error)
95 | """)
96 | return
97 | }
98 |
99 | await transaction.finish()
100 | }
101 |
102 | func checkForUnfinishedTransactions() async {
103 | for await transaction in Transaction.unfinished {
104 | Task.detached(priority: .background) {
105 | await self.process(transaction: transaction)
106 | }
107 | }
108 | }
109 |
110 | func observeTransactionUpdates() async {
111 | for await update in Transaction.updates {
112 | await self.process(transaction: update)
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Model/Store/PassIdentifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PassIdentifiers.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/14/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PassIdentifiers {
11 | var group: String
12 |
13 | var monthly: String
14 | var quarterly: String
15 | var yearly: String
16 | }
17 |
18 | extension EnvironmentValues {
19 | private enum PassIDsKey: EnvironmentKey {
20 | static var defaultValue = PassIdentifiers(
21 | group: "506F71A6",
22 | //group: "21408633",
23 | monthly: "com.pass.monthly",
24 | quarterly: "com.pass.quarterly",
25 | yearly: "com.pass.yearly"
26 | )
27 | }
28 |
29 | var passIDs: PassIdentifiers {
30 | get { self[PassIDsKey.self] }
31 | set { self[PassIDsKey.self] = newValue }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Views/SubscriptionShopContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SubscriptionShopContent.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/15/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SubscriptionShopContent: View {
11 | var body: some View {
12 | VStack {
13 | image
14 | VStack(spacing: 3) {
15 | title
16 | desctiption
17 | }
18 | }
19 | .padding(.vertical)
20 | .padding(.top, 40)
21 | }
22 | }
23 |
24 | #Preview {
25 | SubscriptionShopContent()
26 | }
27 |
28 | extension SubscriptionShopContent {
29 | @ViewBuilder
30 | var image: some View {
31 | Image("movie")
32 | .resizable()
33 | .aspectRatio(contentMode: .fit)
34 | .frame(width: 100)
35 |
36 | }
37 |
38 | @ViewBuilder
39 | var title: some View {
40 | Text("Flower Movie+")
41 | .font(.largeTitle.bold())
42 | }
43 |
44 | @ViewBuilder
45 | var desctiption: some View {
46 | Text("Subscription to unlock all streaming videos, enjoy Blu-ray 4K quality, and watch offline.")
47 | .fixedSize(horizontal: false, vertical: true)
48 | .font(.title3.weight(.medium))
49 | .padding([.bottom, .horizontal])
50 | .foregroundStyle(.gray)
51 | .multilineTextAlignment(.center)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/Views/SubscriptionShopView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SubscriptionShopView.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/14/23.
6 | //
7 |
8 | import SwiftUI
9 | import StoreKit
10 |
11 | struct SubscriptionShopView: View {
12 | @Environment(\.passIDs.group) private var passGroupID
13 |
14 | var body: some View {
15 | SubscriptionStoreView(groupID: passGroupID) {
16 | SubscriptionShopContent()
17 | }
18 | .backgroundStyle(.clear)
19 | .subscriptionStoreButtonLabel(.multiline)
20 | .subscriptionStorePickerItemBackground(.thinMaterial)
21 | .storeButton(.visible, for: .restorePurchases)
22 | }
23 | }
24 |
25 | #Preview {
26 | SubscriptionShopView()
27 | }
28 |
--------------------------------------------------------------------------------
/meet-subscriptionstoreview-in-iOS17/meet-subscriptionstoreview-in-iOS17/meet_subscriptionstoreview_in_iOS17App.swift:
--------------------------------------------------------------------------------
1 | //
2 | // meet_subscriptionstoreview_in_iOS17App.swift
3 | // meet-subscriptionstoreview-in-iOS17
4 | //
5 | // Created by Huang Runhua on 6/14/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct meet_subscriptionstoreview_in_iOS17App: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | .environment(PassStatusModel())
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/struct-initial-macro/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/struct-initial-macro/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/struct-initial-macro/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-syntax",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-syntax.git",
7 | "state" : {
8 | "revision" : "83c2be9f6268e9f67622f130440cf43928c6bfb0",
9 | "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-05-20-a"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/struct-initial-macro/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import CompilerPluginSupport
6 |
7 | let package = Package(
8 | name: "struct-initial-macro",
9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10 | products: [
11 | // Products define the executables and libraries a package produces, making them visible to other packages.
12 | .library(
13 | name: "struct-initial-macro",
14 | targets: ["struct-initial-macro"]
15 | ),
16 | .executable(
17 | name: "struct-initial-macroClient",
18 | targets: ["struct-initial-macroClient"]
19 | ),
20 | ],
21 | dependencies: [
22 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax
23 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
24 | ],
25 | targets: [
26 | // Targets are the basic building blocks of a package, defining a module or a test suite.
27 | // Targets can depend on other targets in this package and products from dependencies.
28 | // Macro implementation that performs the source transformation of a macro.
29 | .macro(
30 | name: "struct-initial-macroMacros",
31 | dependencies: [
32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
34 | ]
35 | ),
36 |
37 | // Library that exposes a macro as part of its API, which is used in client programs.
38 | .target(name: "struct-initial-macro", dependencies: ["struct-initial-macroMacros"]),
39 |
40 | // A client of the library, which is able to use the macro in its own code.
41 | .executableTarget(name: "struct-initial-macroClient", dependencies: ["struct-initial-macro"]),
42 |
43 | // A test target used to develop the macro implementation.
44 | .testTarget(
45 | name: "struct-initial-macroTests",
46 | dependencies: [
47 | "struct-initial-macroMacros",
48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
49 | ]
50 | ),
51 | ]
52 | )
53 |
--------------------------------------------------------------------------------
/struct-initial-macro/Sources/struct-initial-macro/struct_initial_macro.swift:
--------------------------------------------------------------------------------
1 | // The Swift Programming Language
2 | // https://docs.swift.org/swift-book
3 |
4 |
5 | @attached(member, names: named(init))
6 | public macro StructInit() = #externalMacro(module: "struct_initial_macroMacros", type: "StructInitMacro")
7 |
--------------------------------------------------------------------------------
/struct-initial-macro/Sources/struct-initial-macroClient/main.swift:
--------------------------------------------------------------------------------
1 | import struct_initial_macro
2 | import Foundation
3 |
4 | @StructInit
5 | struct Book {
6 | var id: Int
7 | var title: String
8 | var subtitle: String
9 | var description: String
10 | var author: String
11 | }
12 |
--------------------------------------------------------------------------------
/struct-initial-macro/Sources/struct-initial-macroMacros/struct_initial_macroMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import SwiftSyntax
3 | import SwiftSyntaxBuilder
4 | import SwiftSyntaxMacros
5 |
6 | enum StructInitError: CustomStringConvertible, Error {
7 | case onlyApplicableToStruct
8 |
9 | var description: String {
10 | switch self {
11 | case .onlyApplicableToStruct: return "@StructInit can only be applied to a structure"
12 | }
13 | }
14 | }
15 |
16 | public struct StructInitMacro: MemberMacro {
17 | public static func expansion(
18 | of node: AttributeSyntax,
19 | providingMembersOf declaration: some DeclGroupSyntax,
20 | in context: some MacroExpansionContext
21 | ) throws -> [SwiftSyntax.DeclSyntax] {
22 |
23 | guard let structDecl = declaration.as(StructDeclSyntax.self) else {
24 | throw StructInitError.onlyApplicableToStruct
25 | }
26 |
27 | let members = structDecl.memberBlock.members
28 | let variableDecl = members.compactMap { $0.decl.as(VariableDeclSyntax.self) }
29 | let variablesName = variableDecl.compactMap { $0.bindings.first?.pattern }
30 | let variablesType = variableDecl.compactMap { $0.bindings.first?.typeAnnotation?.type }
31 |
32 | let initializer = try InitializerDeclSyntax(StructInitMacro.generateInitialCode(variablesName: variablesName, variablesType: variablesType)) {
33 | for name in variablesName {
34 | ExprSyntax("self.\(name) = \(name)")
35 | }
36 | }
37 |
38 | return [DeclSyntax(initializer)]
39 | }
40 |
41 | public static func generateInitialCode(variablesName: [PatternSyntax],
42 | variablesType: [TypeSyntax]) -> PartialSyntaxNodeString {
43 | var initialCode: String = "init("
44 | for (name, type) in zip(variablesName, variablesType) {
45 | initialCode += "\(name): \(type), "
46 | }
47 | initialCode = String(initialCode.dropLast(2))
48 | initialCode += ")"
49 | return PartialSyntaxNodeString(stringLiteral: initialCode)
50 | }
51 | }
52 |
53 | @main
54 | struct struct_initial_macroPlugin: CompilerPlugin {
55 | let providingMacros: [Macro.Type] = [
56 | StructInitMacro.self,
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/struct-initial-macro/Tests/struct-initial-macroTests/struct_initial_macroTests.swift:
--------------------------------------------------------------------------------
1 | import SwiftSyntaxMacros
2 | import SwiftSyntaxMacrosTestSupport
3 | import XCTest
4 | import struct_initial_macroMacros
5 |
6 | let testMacros: [String: Macro.Type] = [
7 | "StructInit": StructInitMacro.self,
8 | ]
9 |
10 | final class struct_initial_macroTests: XCTestCase {
11 | func testMacro() {
12 | assertMacroExpansion(
13 | """
14 | @StructInit
15 | struct Book {
16 | var id: Int
17 | var title: String
18 | var subtitle: String
19 | var description: String
20 | var author: String
21 | }
22 | """,
23 | expandedSource:
24 | """
25 |
26 | struct Book {
27 | var id: Int
28 | var title: String
29 | var subtitle: String
30 | var description: String
31 | var author: String
32 | init(id: Int, title: String, subtitle: String, description: String, author: String) {
33 | self.id = id
34 | self.title = title
35 | self.subtitle = subtitle
36 | self.description = description
37 | self.author = author
38 | }
39 | }
40 | """,
41 | macros: testMacros
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/struct-initial-macro/article-imgs/pic1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/struct-initial-macro/article-imgs/pic1.png
--------------------------------------------------------------------------------
/swiftdata-example/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/swiftdata-example/.DS_Store
--------------------------------------------------------------------------------
/swiftdata-example/README.md:
--------------------------------------------------------------------------------
1 | # First Glance at SwiftData
2 |
3 | SwiftData enables you to add persistence to your app quickly, with minimal code and no external dependencies. Using modern language features like macros, SwiftData enables you to write code that is fast, efficient, and safe, enabling you to describe the entire model layer (or object graph) for your app. The framework handles storing the underlying model data, and optionally, syncing that data across multiple devices.
4 |
5 | 
6 |
7 | ## Turn classes into models to make them persistable
8 |
9 | To let SwiftData save instances of a model class, import the framework and annotate that class with the `Model` macro. The macro updates the class with conformance to the `PersistentModel` protocol, which SwiftData uses to examine the class and generate an internal schema. Additionally, the macro enables change tracking for the class by adding conformance to the `Observable` protocol.
10 |
11 | ```swift
12 | import SwiftData
13 | // Annotate new or existing model classes with the @Model macro.
14 | @Model
15 | class Note {
16 | var createDate: Date
17 | var title: String
18 | var subtitle: String
19 | var content: String
20 |
21 | init(createDate: Date = .now,
22 | title: String,
23 | subtitle: String,
24 | content: String) {
25 | self.createDate = createDate
26 | self.title = title
27 | self.subtitle = subtitle
28 | self.content = content
29 | }
30 | }
31 | ```
32 |
33 | By default, SwiftData includes all noncomputed properties of a class as long as they use compatible types. The framework supports primitive types such as `Bool`, `Int`, and `String`, as well as complex value types such as structures, enumerations, and other value types that conform to the `Codable` protocol.
34 |
35 | ## Providing Options for @Attribute and @Relationship
36 |
37 | If you want to avoid conflicts in your model data by **specifying that an attribute’s value is unique** across all instances of that model, use `@Attribute(.unique)`:
38 |
39 | ```swift
40 | @Attribute(.unique) var title: String
41 | ```
42 |
43 | If you want to delete all the tags (`Tag` is also a model or a collection of models) when deleting the a note, using `@Relationship(.cascade)` to annotate the property:
44 |
45 | ```swift
46 | @Relationship(.cascade) var tags: [Tag]
47 | ```
48 |
49 | Annotate properties with the `Transient` macro and SwiftData won’t write their values to disk:
50 |
51 | ```swift
52 | @Transient var wordsCount: Int
53 | ```
54 |
55 | ### Specifying original property names
56 |
57 | If you change the name of some variables in your model, that would be seen as a new property in generated schema and SwiftData will create new properties for them. If you want to preserve the existing data you can map the original name to the property name using `@Attribute` and specifying the `originalName:` parameter.
58 |
59 | ```swift
60 | @Attribute(originalName: "subtitle") var description: String
61 | ```
62 |
63 | ## Migration of Models
64 |
65 | As your app’s model layer evolves, SwiftData performs automatic migrations of the underlying model data so it remains in a consistent state. If the aggregate changes between two versions of the model layer exceed the capabilities of automatic migrations, use `Schema` and `SchemaMigrationPlan` to participate in those migrations and help them complete successfully.
66 |
67 | ### Evolving schemas
68 |
69 | - Encapsulate your models at a specific version with `VersionedSchema`
70 | - Order your versions with `SchemaMigrationPlan`
71 | - Define each migration stage
72 |
73 | ### Migration stages
74 |
75 | 1. **Lightweight migration stage**: Lightweight migrations do not require any additional code to migrate the existing data for app release. Modifications like adding new variable to `Note` properties or specifying the delete rules on my relationships are lightweight migration eligible.
76 | 2. **Custom migration stage**: Operations like making the title of a `Note` unique is not eligible for a lightweight migration and require custom migration stage.
77 |
78 | #### Encapsulate original schema in a `VersionedSchema`
79 |
80 | ```swift
81 | enum NoteSchemaV1: VersionedSchema {
82 | static var versionIdentifier: String? = "noteschemav1"
83 | static var models: [any PersistentModel.Type] {
84 | [Note.self]
85 | }
86 | @Model
87 | class Note {
88 | var createDate: Date
89 | var title: String
90 | var subtitle: String
91 | var content: String
92 |
93 | init(createDate: Date = .now, title: String, subtitle: String, content: String) {
94 | self.createDate = createDate
95 | self.title = title
96 | self.subtitle = subtitle
97 | self.content = content
98 | }
99 | }
100 | }
101 | ```
102 |
103 | #### Add NoteSchemaV2
104 |
105 | ```swift
106 | enum NoteSchemaV2: VersionedSchema {
107 | static var versionIdentifier: String? = "noteschemav2"
108 | static var models: [any PersistentModel.Type] {
109 | [Note.self]
110 | }
111 | @Model
112 | class Note {
113 | var createDate: Date
114 | @Attribute(.unique) var title: String
115 | var subtitle: String
116 | var content: String
117 |
118 | init(createDate: Date = .now, title: String, subtitle: String, content: String) {
119 | self.createDate = createDate
120 | self.title = title
121 | self.subtitle = subtitle
122 | self.content = content
123 | }
124 | }
125 | }
126 | ```
127 |
128 | #### Handle migrations
129 |
130 | ```swift
131 | enum NoteMigrationPlan: SchemaMigrationPlan {
132 | static var schemas: [any VersionedSchema.Type] {
133 | // Provide the ordering of schemas.
134 | [NoteSchemaV1.self, NoteSchemaV2.self]
135 | }
136 |
137 | static var stages: [MigrationStage] {
138 | [migrationV1toV2]
139 | }
140 |
141 | static let migrationV1toV2 = MigrationStage.custom(
142 | fromVersion: NoteSchemaV1.self,
143 | toVersion: NoteSchemaV2.self,
144 | willMigrate: { context in
145 | let notes = try? context.fetch(FetchDescriptor())
146 | try? context.save()
147 | }, didMigrate: nil
148 | )
149 | }
150 | ```
151 |
152 | #### Configure the migration plan
153 |
154 | From the WWDC video, you can configure the migration plan through the code below, but it crashed in current version of Xcode 15.0 beta (15A5160n)
155 |
156 | ```swift
157 | @main
158 | struct swiftdata_exampleApp: App {
159 | let container = try! ModelContainer(
160 | for: Schema([Note.self]),
161 | migrationPlan: NoteMigrationPlan.self
162 | )
163 | var body: some Scene {
164 | WindowGroup {
165 | ContentView()
166 | }
167 | .modelContainer(for: Note.self)
168 | }
169 | }
170 | ```
171 |
172 | ## Configure the model storage
173 |
174 | Before SwiftData can examine your models and generate the required schema, you need to tell it — at runtime — which models to persist, and optionally, the configuration to use for the underlying storage.
175 |
176 | To set up the default storage, use the `modelContainer(for:inMemory:isAutosaveEnabled:isUndoEnabled:onSetup:)` view modifier (or the scene equivalent) and specify the array of model types to persist. If you use the view modifier, add it at the very top of the view hierarchy so all nested views inherit the properly configured environment:
177 |
178 | ```swift
179 | import SwiftData
180 | @main
181 | struct swiftdata_exampleApp: App {
182 | var body: some Scene {
183 | WindowGroup {
184 | ContentView()
185 | }
186 | .modelContainer(for: Note.self)
187 | }
188 | }
189 | ```
190 |
191 | If there are more than one models, use list instead:
192 |
193 | ```swift
194 | import SwiftData
195 | @main
196 | struct swiftdata_exampleApp: App {
197 | var body: some Scene {
198 | WindowGroup {
199 | ContentView()
200 | }
201 | .modelContainer(
202 | for: [Note.self, Tag.self]
203 | )
204 | }
205 | }
206 | ```
207 |
208 | **To use SwiftData, any application has to set up at least one ModelContainer.** It creates the whole storage stack, including the context that `@Query` will use. A View has a single model container, but an application can create and use as many containers as it needs for different view hierarchies. If the application does not set up its modelContainer, its windows and the views it creates can not save or query models via SwiftData.
209 |
210 | If your app only has a single model container, the window and its views will inherit the container, as well as any other windows created from the same group. All of these views will write and read from a single container.
211 |
212 | Some apps need a few storage stacks, and they can set up several model containers for different windows.
213 |
214 | ```swift
215 | import SwiftData
216 | @main
217 | struct swiftdata_exampleApp: App {
218 | var body: some Scene {
219 | WindowGroup {
220 | ContentView()
221 | }
222 | .modelContainer(for: [Note.self, Tag.self])
223 |
224 | WindowGroup("Note Deisgner") {
225 | NoteDeisgnerView()
226 | }
227 | .modelContainer(for: Designer.self)
228 | }
229 | }
230 | ```
231 |
232 | SwiftUI also allows for a granular setup on a view level. Different views in the same window can have separate containers, and saving in one container won’t affect another.
233 |
234 | ```swift
235 | struct AnotherView: View {
236 | var body: some View {
237 | ScrollView {
238 | Form {...}
239 | LibraryView()
240 | .modelContainer(for: Library.self)
241 | }
242 | }
243 | }
244 | ```
245 |
246 | ## Save models for later use
247 |
248 | To manage instances of your model classes at runtime, use a *model context* — the object responsible for the in-memory model data and coordination with the model container to successfully persist that data. To get a context for your model container that’s bound to the main actor, use the `modelContext` environment variable:
249 |
250 | ```swift
251 | import SwiftData
252 | struct NoteEditView: View {
253 | @Environment(\.modelContext) private var modelContext
254 | ...
255 | }
256 | ```
257 |
258 | ### Add New Data
259 |
260 | To enable SwiftData to persist a model instance and begin tracking changes to it, insert the instance into the context:
261 |
262 | ```swift
263 | var note: Note = Note(title: title,
264 | subtitle: subtitle,
265 | content: content)
266 | context.insert(note)
267 | ```
268 |
269 | > Note that the default behavior of context will be auto-save. If you don't want context to auto-save your data, use `.modelContainer(isAutosaveEnabled:false)` .
270 |
271 | Following the insert, you can save immediately by invoking the context’s `save()`method, or rely on the context’s implicit save behavior instead. Contexts automatically track changes to their known model instances and include those changes in subsequent saves. In addition to saving, you can use a context to fetch, enumerate, and delete model instances.
272 |
273 | ### Delete Data
274 |
275 | ```swift
276 | context.delete(note)
277 | ```
278 |
279 | ### Manually Save Changes
280 |
281 | ```swift
282 | try context.save()
283 | ```
284 |
285 | ### Update Data Automatically
286 |
287 | Pass your data with the `@Bindable` macro, when something change the data in the database will change as well:
288 |
289 | ```swift
290 | struct NoteEditView: View {
291 | @Bindable var note: Note
292 | }
293 | ```
294 |
295 | ## Fetch models for display or additional processing
296 |
297 | After you begin persisting model data, you’ll likely want to retrieve that data, materialized as model instances, and display those instances in a view or take some other action on them. SwiftData provides the `Query` property wrapper and the `FetchDescriptor` type for performing fetches.
298 |
299 | To fetch model instances, and optionally apply search criteria and a preferred sort order, use `@Query` in your SwiftUI view. The `@Model` macro adds `Observable` conformance to your model classes, enabling SwiftUI to refresh the containing view whenever changes occur to any of the fetched instances.
300 |
301 | ### Using @Query to load and filter data
302 |
303 | ```swift
304 | import SwiftData
305 | struct ContentView: View {
306 | @Query(sort: \.createDate, order: .reverse) private var notes: [Note]
307 | ...
308 | }
309 | ```
310 |
311 | ### Fetch the specific data
312 |
313 | Use `Predicate` for searching or filtering your database.
314 |
315 | ```swift
316 | let notePredictate = #Predicate { note in
317 | note.title.count > 5
318 | }
319 |
320 | @Query(filter: notePredictate, sort: \.createDate, order: .reverse) private var notes: [Note]
321 | ```
322 |
323 | ## Preview in SwiftUI
324 |
325 | If you want to preview some sample data in Xcode using SwiftUI, you may need to create a preview container.
326 |
327 | ### Create preview container
328 |
329 | ```swift
330 | import SwiftData
331 | @MainActor
332 | let previewContainer: ModelContainer = {
333 | do {
334 | let container = try ModelContainer(
335 | for: Note.self, ModelConfiguration(inMemory: true)
336 | )
337 | for note in SampleNotes.contents {
338 | container.mainContext.insert(object: note)
339 | }
340 | return container
341 | } catch {
342 | fatalError("Failed to create container")
343 | }
344 | }()
345 |
346 | struct SampleNotes {
347 | static var contents: [Note] = [...]
348 | }
349 | ```
350 |
351 | You can also declare the container in the following way:
352 |
353 | ```swift
354 | actor PreviewSampleData {
355 | @MainActor
356 | static var previewContainer: ModelContainer = {
357 | do {
358 | let container = try ModelContainer(
359 | for: Note.self, ModelConfiguration(inMemory: true)
360 | )
361 | for note in SampleNotes.contents {
362 | container.mainContext.insert(object: note)
363 | }
364 | return container
365 | } catch {
366 | fatalError("Failed to create container")
367 | }
368 | }()
369 | }
370 | ```
371 |
372 | If there are more than one models in your app, you can migrate two containers in one:
373 |
374 | ```swift
375 | actor PreviewSampleData {
376 | @MainActor
377 | static var container: ModelContainer = {
378 | let schema = Schema([Note.self, Tag.self])
379 | let configuration = ModelConfiguration(inMemory: true)
380 | let container = try! ModelContainer(for: schema, configurations: [configuration])
381 | let sampleData: [any PersistentModel] = [
382 | Note.preview, Tag.preview
383 | ]
384 | sampleData.forEach {
385 | container.mainContext.insert($0)
386 | }
387 | return container
388 | }()
389 | }
390 |
391 | /// Declare the preview data inside the models
392 | extension Note {
393 | static var preview: Note {
394 | Note(...)
395 | }
396 | }
397 |
398 | extension Tag {
399 | static var preview: Tag {
400 | Tag(...)
401 | }
402 | }
403 | ```
404 |
405 | ### Enable preview in SwiftUI
406 |
407 | #### Display all the notes
408 |
409 | ```swift
410 | #Preview {
411 | /// Xcode15.0 beta (15A5160n)
412 | MainActor.assumeIsolated {
413 | ContentView()
414 | .modelContainer(previewContainer)
415 | }
416 | /// Later may change to
417 | ContentView()
418 | .modelContainer(previewContainer)
419 | }
420 | ```
421 |
422 | #### Display single note
423 |
424 | ```swift
425 | struct NotePreviewCell: View {
426 | var note: Note
427 | ...
428 | }
429 |
430 | #Preview {
431 | /// This is the method provided by Apple's example code
432 | /// but still not working in Xcode15.0 beta (15A5160n)
433 | MainActor.assumeIsolated {
434 | NotePreviewCell(note: .preview)
435 | .modelContainer(PreviewSampleData.previewContainer)
436 | }
437 | }
438 |
439 | /// This way works.
440 | #Preview {
441 | MainActor.assumeIsolated {
442 | let container = PreviewSampleLedgerData.ledgerPreviewContainer
443 | return NotePreviewCell(note: .preview)
444 | .modelContainer(container)
445 | }
446 | }
447 | ```
448 |
449 |
--------------------------------------------------------------------------------
/swiftdata-example/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/swiftdata-example/cover.jpg
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HuangRunHua/wwdc23-code-notes/01daaf05c7673314a105811a79a90334a17e6afc/swiftdata-example/swiftdata-example.xcodeproj/project.xcworkspace/xcuserdata/huangrunhua.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example.xcodeproj/xcuserdata/huangrunhua.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | swiftdata-example.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/11/23.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftData
10 |
11 | //let notePredictate = #Predicate { note in
12 | // note.title.count > 5
13 | //}
14 |
15 | struct ContentView: View {
16 | @Query(sort: \.createDate, order: .reverse) private var notes: [Note]
17 | @State private var showNewNoteView: Bool = false
18 | @Environment(\.modelContext) private var modelContext
19 |
20 | var body: some View {
21 | NavigationView {
22 | List {
23 | ForEach(notes) { note in
24 | ZStack(alignment: .leading) {
25 | NavigationLink {
26 | NoteView(note: note)
27 | } label: {
28 | EmptyView()
29 | }
30 | .opacity(0)
31 | NotePreviewCell(note: note)
32 | }
33 | .swipeActions(edge: .trailing) {
34 | Button(role: .destructive) {
35 | // Known Issue in iOS 17:
36 | // After deleting an item, SwiftUI may attempt to reference
37 | // the deleted content during the animation causing a crash. (109838173)
38 | // Workaround: The workaround is to explicitly save after a delete.
39 | modelContext.delete(note)
40 | try? modelContext.save()
41 | } label: {
42 | Label("Delete", systemImage: "trash")
43 | }
44 | }
45 | }
46 | }
47 | .listStyle(.plain)
48 | .navigationTitle("Notes")
49 | .toolbar {
50 | ToolbarItem(placement: .topBarTrailing) {
51 | Button(action: {
52 | showNewNoteView = true
53 | }, label: {
54 | Image(systemName: "square.and.pencil")
55 | })
56 | }
57 | }
58 | .overlay {
59 | if notes.isEmpty {
60 | ContentUnavailableView {
61 | Label("No Nots", systemImage: "square.and.pencil.circle")
62 | } description: {
63 | Text("New notes you create will appear here.")
64 | }
65 | }
66 | }
67 | }
68 | .sheet(isPresented: $showNewNoteView, content: {
69 | let note = Note(title: "", subtitle: "", content: "")
70 | NoteEditView(edit: false,
71 | note: note,
72 | title: note.title,
73 | subtitle: note.subtitle,
74 | content: note.content)
75 | })
76 | }
77 | }
78 |
79 | #Preview {
80 | MainActor.assumeIsolated {
81 | ContentView()
82 | .modelContainer(PreviewSampleData.previewContainer)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/ModelData/Note.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Note.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/11/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model
12 | class Note {
13 | var createDate: Date
14 | var title: String
15 | var subtitle: String
16 | var content: String
17 |
18 | init(createDate: Date = .now, title: String, subtitle: String, content: String) {
19 | self.createDate = createDate
20 | self.title = title
21 | self.subtitle = subtitle
22 | self.content = content
23 | }
24 | }
25 |
26 | extension Note {
27 | static var preview: Note {
28 | Note(title: "Preserving your app’s model data across launches",
29 | subtitle: "Describe your model classes to SwiftData using the framework’s macros, and store instances of those models so they exist beyond the app’s runtime.",
30 | content: "Most apps define a number of custom types that model the data it creates or consumes. For example, a travel app might define classes that represent trips, flights, and booked accommodations. Using SwiftData, you can quickly and efficiently persist that data so it’s available across app launches, and leverage the framework’s integration with SwiftUI to refetch that data and display it onscreen.")
31 | }
32 | }
33 |
34 | enum NoteSchemaV1: VersionedSchema {
35 | static var versionIdentifier: String? = "noteschemav1"
36 |
37 | static var models: [any PersistentModel.Type] {
38 | [Note.self]
39 | }
40 |
41 | @Model
42 | class Note {
43 | var createDate: Date
44 | var title: String
45 | var subtitle: String
46 | var content: String
47 |
48 | init(createDate: Date = .now, title: String, subtitle: String, content: String) {
49 | self.createDate = createDate
50 | self.title = title
51 | self.subtitle = subtitle
52 | self.content = content
53 | }
54 | }
55 | }
56 |
57 | enum NoteSchemaV2: VersionedSchema {
58 | static var versionIdentifier: String? = "noteschemav2"
59 |
60 | static var models: [any PersistentModel.Type] {
61 | [Note.self]
62 | }
63 |
64 | @Model
65 | class Note {
66 | var createDate: Date
67 | @Attribute(.unique) var title: String
68 | var subtitle: String
69 | var content: String
70 |
71 | init(createDate: Date = .now, title: String, subtitle: String, content: String) {
72 | self.createDate = createDate
73 | self.title = title
74 | self.subtitle = subtitle
75 | self.content = content
76 | }
77 | }
78 | }
79 |
80 | enum NoteMigrationPlan: SchemaMigrationPlan {
81 | static var schemas: [any VersionedSchema.Type] {
82 | // Provide the ordering of schemas.
83 | [NoteSchemaV1.self, NoteSchemaV2.self]
84 | }
85 |
86 | static var stages: [MigrationStage] {
87 | [migrationV1toV2]
88 | }
89 |
90 | static let migrationV1toV2 = MigrationStage.custom(
91 | fromVersion: NoteSchemaV1.self,
92 | toVersion: NoteSchemaV2.self,
93 | willMigrate: { context in
94 | let notes = try? context.fetch(FetchDescriptor())
95 | try? context.save()
96 | }, didMigrate: nil
97 | )
98 | }
99 |
100 |
101 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/ModelData/NotePreviewSampleData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotePreviewSampleData.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/17/23.
6 | //
7 |
8 | import SwiftData
9 |
10 |
11 | actor PreviewSampleData {
12 | @MainActor
13 | static var previewContainer: ModelContainer = {
14 | do {
15 | let container = try ModelContainer(
16 | for: Note.self, ModelConfiguration(inMemory: true)
17 | )
18 | for note in SampleNotes.contents {
19 | container.mainContext.insert(object: note)
20 | }
21 | return container
22 | } catch {
23 | fatalError("Failed to create container")
24 | }
25 | }()
26 | }
27 |
28 | struct SampleNotes {
29 | static var contents: [Note] = [
30 | Note(title: "How Britain can become an AI superpower",
31 | subtitle: "Rishi Sunak’s enthusiasm is welcome. But his plans for Britain fall short",
32 | content: "Get ready for some big British celebrations in 2030. By then, if Rishi Sunak is to be believed, the country will be “a science and technology superpower”. The prime minister’s aim is for Britain to prosper from the booming opportunities offered by supercomputing and artificial intelligence. Generative ai has stoked a frenzy of excitement (and some fear) among techies and investors; now politicians have started to acclaim its potential, and British ones are in the vanguard. Britain, says Mr Sunak, will harness ai and thus spur productivity, economic growth and more. As he told an audience in London this week, he sees the “extraordinary potential of ai to improve people’s lives”."),
33 | Note(title: "Is the global housing slump over?",
34 | subtitle: "Why rising interest rates have not yet triggered property pandemonium",
35 | content: "In australia house prices have risen for the past three months. In America a widely watched index of housing values has risen by 1.6% from its low in January, and housebuilders’ share prices have done twice as well as the overall stockmarket. In the euro area the property market looks steady. “[M]ost of the drag from housing on gdp growth from now on should be marginal,” wrote analysts at jpmorgan Chase, a bank, in a recent report about America. “[W]e believe the peak negative drag from the recent housing-market slump to private consumption is likely behind us,” wrote wonks at Goldman Sachs, another bank, about South Korea."),
36 | Note(title: "How long will the travel boom last?",
37 | subtitle: "Will demand for sunny getaways wane with economic turbulence?",
38 | content: "Revenge holidays are in full swing and the travel industry is cashing in. After a rocky few years, the urge to splurge on airline tickets and hotels is set to bring in bumper earnings. Tour operators are inundated with bookings; hotel chains are raking in record profits. EasyJet has raised its earnings forecasts twice this year; iag and Ryanair have both returned to profit for the first time since the start of the pandemic, and Singapore Airlines is handing out some of its record profits as bonuses worth eight months’ salary. With air fares rising faster than inflation, global airline bosses now expect $9.8bn in net income this year, more than double the amount initially forecast, according to the International Air Transport Association, an industry body.")
39 | ]
40 | }
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Views/NoteEditView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NoteEditView.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/17/23.
6 | //
7 |
8 | // Bugs:
9 | // 1. 进入编辑界面时候无论什么操作都会自动被保存
10 | // Shartage:
11 | // 1. 删除功能
12 |
13 | import SwiftUI
14 | import SwiftData
15 |
16 | struct NoteEditView: View {
17 |
18 | let edit: Bool
19 | var note: Note
20 |
21 | @Environment(\.dismiss) var dismiss
22 | @Environment(\.modelContext) private var modelContext
23 |
24 | @State var title: String = ""
25 | @State var subtitle: String = ""
26 | @State var content: String = ""
27 |
28 | var body: some View {
29 | NavigationView {
30 | ScrollView {
31 | VStack {
32 | HStack(alignment: .top) {
33 | MarkerView(text: "H1").frame(width: 20)
34 | TextField("Enter title...", text: $title, axis: .vertical)
35 | .font(.title)
36 | .bold()
37 | }
38 | HStack(alignment: .top) {
39 | MarkerView(text: "H2").frame(width: 20)
40 | TextField("Enter subtitle...", text: $subtitle, axis: .vertical)
41 | .font(.headline)
42 | }
43 | .padding(.top, -7)
44 | HStack(alignment: .top) {
45 | MarkerView(text: "B1").frame(width: 20)
46 | TextField("Enter content...", text: $content, axis: .vertical)
47 | .foregroundStyle(Color.init(UIColor.darkGray))
48 |
49 | }
50 | .padding(.top, -3)
51 | }
52 | .fontDesign(.rounded)
53 | .padding()
54 | }
55 | .navigationBarTitleDisplayMode(.inline)
56 | .toolbar {
57 | ToolbarItem(placement: .topBarLeading) {
58 | Button("Cancel") {
59 | dismiss()
60 | }
61 | }
62 | ToolbarItem(placement: .topBarTrailing) {
63 | Button("Done") {
64 | if edit {
65 | note.title = title
66 | note.subtitle = subtitle
67 | note.content = content
68 | } else {
69 | note.title = title
70 | note.subtitle = subtitle
71 | note.content = content
72 | modelContext.insert(object: note)
73 | }
74 | dismiss()
75 | }
76 | .disabled(title=="" ? true: false)
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | struct MarkerView: View {
84 | let text: String
85 | var body: some View {
86 | HStack(alignment: .bottom, spacing: 0) {
87 | Text(text.split(separator: "").first ?? "")
88 | .foregroundStyle(.gray)
89 | Text(text.split(separator: "").last ?? "")
90 | .font(.system(size: 13))
91 | .foregroundStyle(.gray)
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Views/NotePreviewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotePreviewCell.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/17/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NotePreviewCell: View {
11 | var note: Note
12 | var body: some View {
13 | VStack(alignment: .leading) {
14 | HStack(alignment: .top) {
15 | Text(note.title)
16 | .bold()
17 | .lineLimit(1)
18 | Spacer()
19 | Text("\(note.createDate.formatted(date: .omitted, time: .shortened)) \(Image(systemName: "chevron.right"))")
20 | .foregroundStyle(.gray)
21 | }
22 | Text(note.subtitle)
23 | .lineLimit(1)
24 | Text(note.content)
25 | .lineLimit(2)
26 | .foregroundStyle(.gray)
27 | }
28 | }
29 | }
30 |
31 | #Preview {
32 | MainActor.assumeIsolated {
33 | NotePreviewCell(note: .preview)
34 | .modelContainer(PreviewSampleData.previewContainer)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/Views/NoteView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NoteView.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/17/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NoteView: View {
11 |
12 | var note: Note
13 | @State private var showEditView: Bool = false
14 |
15 | var body: some View {
16 | ScrollView {
17 | Text("\(note.createDate.formatted(date: .abbreviated, time: .shortened))")
18 | .foregroundStyle(.gray)
19 | HStack {
20 | Text(note.title)
21 | .font(.title)
22 | .bold()
23 | Spacer()
24 | }
25 | .padding([.leading,.trailing])
26 | HStack {
27 | Text(note.subtitle)
28 | .font(.headline)
29 | .padding([.leading,.trailing])
30 | .padding(.top, -7)
31 | Spacer()
32 | }
33 | HStack {
34 | Text(note.content)
35 | .foregroundStyle(Color.init(UIColor.darkGray))
36 | .padding([.leading,.trailing])
37 | .padding(.top, -3)
38 | .lineSpacing(5)
39 | Spacer()
40 | }
41 | }
42 | .fontDesign(.rounded)
43 | .navigationBarTitleDisplayMode(.inline)
44 | .toolbar {
45 | ToolbarItem(placement: .topBarTrailing) {
46 | Button(action: {
47 | showEditView = true
48 | }, label: {
49 | Text("Edit")
50 | })
51 | }
52 | }
53 | .sheet(isPresented: $showEditView, content: {
54 | NoteEditView(edit: true,
55 | note: note,
56 | title: note.title,
57 | subtitle: note.subtitle,
58 | content: note.content)
59 | })
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/swiftdata-example/swiftdata-example/swiftdata_exampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // swiftdata_exampleApp.swift
3 | // swiftdata-example
4 | //
5 | // Created by Huang Runhua on 6/11/23.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftData
10 |
11 | @main
12 | struct swiftdata_exampleApp: App {
13 |
14 | // let container = try! ModelContainer(
15 | // for: Schema([Note.self]),
16 | // migrationPlan: NoteMigrationPlan.self
17 | // )
18 |
19 | var body: some Scene {
20 |
21 | WindowGroup {
22 | ContentView()
23 | }
24 | //.modelContainer(container)
25 | .modelContainer(for: Note.self)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------