├── .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 | ![](https://github.com/HuangRunHua/wwdc23-code-notes/raw/main/cover.jpg) 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 | ![](https://github.com/HuangRunHua/wwdc23-code-notes/raw/main/discover-calendar-and-eventkit/images/IMG_7703.JPEG) 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 | ![](https://github.com/HuangRunHua/wwdc23-code-notes/raw/main/discover-observation-in-swiftui/pic1.jpg) 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 | ![](https://github.com/HuangRunHua/wwdc23-code-notes/raw/main/lift-subjects-from-images/cover.png)
 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 | ![](https://miro.medium.com/v2/resize:fit:1024/format:webp/1*9D03J3Lys0ykNfQf5ewR2A.png) 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 | ![](https://github.com/HuangRunHua/wwdc23-code-notes/raw/main/swiftdata-example/cover.jpg) 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 | --------------------------------------------------------------------------------