├── .gitignore ├── CoordinatorSwiftUI WatchKit App ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_1024@1x.png │ │ ├── icon_24@2x.png │ │ ├── icon_27.5@2x.png │ │ ├── icon_29@2x.png │ │ ├── icon_29@3x.png │ │ ├── icon_40@2x.png │ │ ├── icon_86@2x.png │ │ └── icon_98@2x.png │ └── Contents.json ├── Base.lproj │ └── Interface.storyboard └── Info.plist ├── CoordinatorSwiftUI WatchKit Extension ├── Assets.xcassets │ ├── Complication.complicationset │ │ ├── Circular.imageset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Extra Large.imageset │ │ │ └── Contents.json │ │ ├── Graphic Bezel.imageset │ │ │ └── Contents.json │ │ ├── Graphic Circular.imageset │ │ │ └── Contents.json │ │ ├── Graphic Corner.imageset │ │ │ └── Contents.json │ │ ├── Graphic Large Rectangular.imageset │ │ │ └── Contents.json │ │ ├── Modular.imageset │ │ │ └── Contents.json │ │ └── Utilitarian.imageset │ │ │ └── Contents.json │ └── Contents.json ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── SourceCode │ ├── CommonViews │ └── ContextMenu │ │ ├── LogoutButtonView.swift │ │ └── RefreshButtonView.swift │ ├── Coordinators │ ├── ApplicationCoordinator.swift │ └── Coordinator.swift │ ├── ExtensionDelegate │ └── ExtensionDelegate.swift │ ├── Extensions │ ├── Binding+Convenience.swift │ └── View+Convertion.swift │ ├── LifeCycle │ ├── ApplicationView.swift │ └── HostingController.swift │ ├── Managers │ └── UserManager.swift │ ├── Models │ ├── Author.swift │ ├── Thumbnail.swift │ └── User.swift │ ├── Modules │ ├── Authentication │ │ ├── Coordinators │ │ │ └── AuthenticationCoordinator.swift │ │ ├── ViewModels │ │ │ └── UserListViewModel.swift │ │ └── Views │ │ │ └── AuthenticationView.swift │ └── Authors │ │ ├── Coordinators │ │ └── AuthorsCoordinator.swift │ │ └── Modules │ │ ├── AuthorProfileDetail │ │ ├── ViewModels │ │ │ └── AuthorProfileDetailViewModel.swift │ │ └── Views │ │ │ └── AuthorProfileDetailView.swift │ │ └── AuthorsList │ │ ├── Enum │ │ └── AuthorsListViewModel+State.swift │ │ ├── ViewModels │ │ └── AuthorsListViewModel.swift │ │ └── Views │ │ ├── AuthorRowView.swift │ │ └── AuthorsListView.swift │ ├── Protocols │ └── KeyPathUpdatable.swift │ ├── Providers │ └── Authors │ │ ├── AuthorsProvider.swift │ │ └── Mocks │ │ ├── AuthorsProviderFailureMock.swift │ │ └── AuthorsProviderSuccessMock.swift │ ├── Stores │ └── AuthorsStore.swift │ └── Stubs │ ├── Authors │ └── Authors.json │ ├── AuthorsStub.swift │ ├── File.swift │ ├── Users │ └── Users.json │ └── UsersStub.swift ├── CoordinatorSwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── CoordinatorSwiftUI WatchKit App.xcscheme └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/xcode,swift 3 | # Edit at https://www.gitignore.io/?templates=xcode,swift 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Xcode Patch 32 | *.xcodeproj/* 33 | !*.xcodeproj/project.pbxproj 34 | !*.xcodeproj/xcshareddata/ 35 | !*.xcworkspace/contents.xcworkspacedata 36 | /*.gcno 37 | 38 | ### Xcode Patch ### 39 | **/xcshareddata/WorkspaceSettings.xcsettings 40 | 41 | # End of https://www.gitignore.io/api/xcode,swift 42 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "24x24", 5 | "idiom" : "watch", 6 | "filename" : "icon_24@2x.png", 7 | "scale" : "2x", 8 | "role" : "notificationCenter", 9 | "subtype" : "38mm" 10 | }, 11 | { 12 | "size" : "27.5x27.5", 13 | "idiom" : "watch", 14 | "filename" : "icon_27.5@2x.png", 15 | "scale" : "2x", 16 | "role" : "notificationCenter", 17 | "subtype" : "42mm" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "idiom" : "watch", 22 | "filename" : "icon_29@2x.png", 23 | "role" : "companionSettings", 24 | "scale" : "2x" 25 | }, 26 | { 27 | "size" : "29x29", 28 | "idiom" : "watch", 29 | "filename" : "icon_29@3x.png", 30 | "role" : "companionSettings", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "watch", 36 | "filename" : "icon_40@2x.png", 37 | "scale" : "2x", 38 | "role" : "appLauncher", 39 | "subtype" : "38mm" 40 | }, 41 | { 42 | "size" : "44x44", 43 | "idiom" : "watch", 44 | "scale" : "2x", 45 | "role" : "appLauncher", 46 | "subtype" : "40mm" 47 | }, 48 | { 49 | "size" : "50x50", 50 | "idiom" : "watch", 51 | "scale" : "2x", 52 | "role" : "appLauncher", 53 | "subtype" : "44mm" 54 | }, 55 | { 56 | "size" : "86x86", 57 | "idiom" : "watch", 58 | "filename" : "icon_86@2x.png", 59 | "scale" : "2x", 60 | "role" : "quickLook", 61 | "subtype" : "38mm" 62 | }, 63 | { 64 | "size" : "98x98", 65 | "idiom" : "watch", 66 | "filename" : "icon_98@2x.png", 67 | "scale" : "2x", 68 | "role" : "quickLook", 69 | "subtype" : "42mm" 70 | }, 71 | { 72 | "size" : "108x108", 73 | "idiom" : "watch", 74 | "scale" : "2x", 75 | "role" : "quickLook", 76 | "subtype" : "44mm" 77 | }, 78 | { 79 | "size" : "1024x1024", 80 | "idiom" : "watch-marketing", 81 | "filename" : "icon_1024@1x.png", 82 | "scale" : "1x" 83 | } 84 | ], 85 | "info" : { 86 | "version" : 1, 87 | "author" : "xcode" 88 | } 89 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_1024@1x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_24@2x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_27.5@2x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_29@2x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_29@3x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_40@2x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_86@2x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnamedd/CoordinatorSwiftUI/f9b02587ac931007650a16c3b9bcf4ae2e52f7ae/CoordinatorSwiftUI WatchKit App/Assets.xcassets/AppIcon.appiconset/icon_98@2x.png -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Base.lproj/Interface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Authors 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | UISupportedInterfaceOrientations 24 | 25 | UIInterfaceOrientationPortrait 26 | UIInterfaceOrientationPortraitUpsideDown 27 | 28 | WKWatchKitApp 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "idiom" : "watch", 5 | "filename" : "Circular.imageset", 6 | "role" : "circular" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "filename" : "Extra Large.imageset", 11 | "role" : "extra-large" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "filename" : "Graphic Bezel.imageset", 16 | "role" : "graphic-bezel" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "filename" : "Graphic Circular.imageset", 21 | "role" : "graphic-circular" 22 | }, 23 | { 24 | "idiom" : "watch", 25 | "filename" : "Graphic Corner.imageset", 26 | "role" : "graphic-corner" 27 | }, 28 | { 29 | "idiom" : "watch", 30 | "filename" : "Graphic Large Rectangular.imageset", 31 | "role" : "graphic-large-rectangular" 32 | }, 33 | { 34 | "idiom" : "watch", 35 | "filename" : "Modular.imageset", 36 | "role" : "modular" 37 | }, 38 | { 39 | "idiom" : "watch", 40 | "filename" : "Utilitarian.imageset", 41 | "role" : "utilitarian" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">161" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "scale" : "2x", 16 | "screen-width" : ">145" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "scale" : "2x", 21 | "screen-width" : ">183" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | CoordinatorSwiftUI WatchKit Extension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | WKAppBundleIdentifier 28 | codes.unnamedd.CoordinatorSwiftUI.watchkitapp 29 | 30 | NSExtensionPointIdentifier 31 | com.apple.watchkit 32 | 33 | WKExtensionDelegateClassName 34 | $(PRODUCT_MODULE_NAME).ExtensionDelegate 35 | WKWatchOnly 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/CommonViews/ContextMenu/LogoutButtonView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LogoutButtonView: View { 4 | var body: some View { 5 | let configuration = UIImage.SymbolConfiguration(pointSize: 30, weight: .light, scale: .large) 6 | let compose = UIImage(systemName: "square.and.pencil", withConfiguration: configuration)! 7 | 8 | return ZStack { 9 | Image(uiImage: compose) 10 | Text("Logout") 11 | .fontWeight(.bold) 12 | } 13 | } 14 | } 15 | 16 | struct LogoutButtonView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | LogoutButtonView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/CommonViews/ContextMenu/RefreshButtonView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct RefreshButtonView: View { 4 | var body: some View { 5 | let configuration = UIImage.SymbolConfiguration(pointSize: 30, weight: .light, scale: .large) 6 | let compose = UIImage(systemName: "arrow.counterclockwise.circle", withConfiguration: configuration)! 7 | 8 | return ZStack { 9 | Image(uiImage: compose) 10 | Text("Refresh") 11 | .fontWeight(.bold) 12 | } 13 | } 14 | } 15 | 16 | struct RefreshButtonView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | RefreshButtonView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Coordinators/ApplicationCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | final class ApplicationCoordinator: Coordinator, ObservableObject { 5 | 6 | @Published 7 | private var userManager = UserManager.shared 8 | 9 | // MARK: - Init 10 | override init() { 11 | super.init() 12 | } 13 | 14 | override func start() -> AnyView { 15 | guard userManager.isUserAuthenticated else { 16 | return startAuthentication() 17 | } 18 | 19 | return startAuthors() 20 | } 21 | 22 | func startAuthentication() -> AnyView { 23 | let authenticationCoordinator = AuthenticationCoordinator() 24 | authenticationCoordinator.delegate = self 25 | 26 | addChild(authenticationCoordinator) 27 | 28 | return authenticationCoordinator.start() 29 | } 30 | 31 | func startAuthors() -> AnyView { 32 | let authorsCoordinator = AuthorsCoordinator() 33 | authorsCoordinator.delegate = self 34 | 35 | addChild(authorsCoordinator) 36 | 37 | return authorsCoordinator.start() 38 | } 39 | } 40 | 41 | // MARK: - AuthenticationCoordinator Delegate 42 | 43 | extension ApplicationCoordinator: AuthenticationCoordinatorDelegate { 44 | func authenticationCoordinator(_ coordinator: AuthenticationCoordinator, didFinishAuthentication user: User) { 45 | removeChild(coordinator) 46 | 47 | userManager.set(user) 48 | self.objectWillChange.send() 49 | } 50 | } 51 | 52 | // MARK: - AuthorsCoordinator Delegate 53 | 54 | extension ApplicationCoordinator: AuthorsCoordinatorDelegate { 55 | func authorsCoordinatorDidFinish(_ coordinator: AuthorsCoordinator) { 56 | removeChild(coordinator) 57 | 58 | userManager.clearData() 59 | self.objectWillChange.send() 60 | } 61 | } 62 | 63 | 64 | 65 | // MARK: - Dummy 66 | 67 | #if DEBUG 68 | 69 | extension ApplicationCoordinator { 70 | static func makeDummy() -> ApplicationCoordinator { 71 | ApplicationCoordinator() 72 | } 73 | } 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Coordinators/Coordinator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | open class Coordinator { 4 | 5 | weak var parent: Coordinator? 6 | private(set) var children: Set 7 | 8 | init() { 9 | children = Set() 10 | } 11 | 12 | final func addChild(_ coordinator: Coordinator) { 13 | coordinator.parent = self 14 | children.insert(coordinator) 15 | } 16 | 17 | final func removeChild(_ coordinator: Coordinator) { 18 | children.remove(coordinator) 19 | } 20 | 21 | final func removeAllChildren() { 22 | children.removeAll() 23 | } 24 | 25 | open func start() -> AnyView { 26 | fatalError("Subclasses must implement \(#function)") 27 | } 28 | } 29 | 30 | extension Coordinator: Hashable { 31 | public func hash(into hasher: inout Hasher) { 32 | hasher.combine(ObjectIdentifier(self).hashValue) 33 | } 34 | } 35 | 36 | extension Coordinator: Equatable { 37 | public static func ==(lhs: Coordinator, rhs: Coordinator) -> Bool { 38 | return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/ExtensionDelegate/ExtensionDelegate.swift: -------------------------------------------------------------------------------- 1 | import WatchKit 2 | 3 | final class ExtensionDelegate: NSObject, WKExtensionDelegate {} 4 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Extensions/Binding+Convenience.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Binding { 4 | init(_ object: Object, keyPath: ReferenceWritableKeyPath) { 5 | self.init(get: { object[keyPath: keyPath] }) { newValue in 6 | object[keyPath: keyPath] = newValue 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Extensions/View+Convertion.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | 5 | /// Convert any `View` into `AnyView` 6 | var any: AnyView { 7 | AnyView(self) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/LifeCycle/ApplicationView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ApplicationView: View { 4 | @ObservedObject var coordinator: ApplicationCoordinator 5 | 6 | var body: some View { 7 | coordinator.start() 8 | } 9 | } 10 | 11 | // MARK: - Dummy 12 | 13 | #if DEBUG 14 | 15 | struct ApplicationView_Previews: PreviewProvider { 16 | static var previews: some View { 17 | ApplicationView( 18 | coordinator: ApplicationCoordinator.makeDummy() 19 | ) 20 | } 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/LifeCycle/HostingController.swift: -------------------------------------------------------------------------------- 1 | import WatchKit 2 | import SwiftUI 3 | 4 | final class HostingController: WKHostingController { 5 | var coordinator: ApplicationCoordinator = ApplicationCoordinator() 6 | 7 | override var body: ApplicationView { 8 | ApplicationView( 9 | coordinator: coordinator 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Managers/UserManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class UserManager { 4 | private let userDefaults = UserDefaults.standard 5 | 6 | private var user: User? 7 | 8 | static let shared = UserManager() 9 | 10 | private init() {} 11 | 12 | var isUserAuthenticated: Bool { 13 | user != nil 14 | } 15 | 16 | func clearData() { 17 | user = nil 18 | } 19 | 20 | func set(_ user: User) { 21 | self.user = user 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Models/Author.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias Authors = [Author] 4 | 5 | struct Author: Codable, Identifiable { 6 | let id: String 7 | let position: Int 8 | let name: String 9 | let minimumEstimatedSales: String 10 | let maximumEstimatedSales: String 11 | let originalLanguage: String 12 | let genreOrMajorWorks: String 13 | let numberOfBooks: String 14 | let nationality: String 15 | var canonical: String 16 | var thumbnail: Thumbnail? 17 | var wikipedia: String 18 | var description: String 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case id 22 | case position 23 | case name = "author" 24 | case minimumEstimatedSales = "minimum_estimated_sales" 25 | case maximumEstimatedSales = "maximum_estimated_sales" 26 | case originalLanguage = "original_language" 27 | case genreOrMajorWorks = "genre_or_major_works" 28 | case numberOfBooks = "number_of_books" 29 | case nationality 30 | case canonical 31 | case thumbnail 32 | case wikipedia 33 | case description 34 | } 35 | } 36 | 37 | extension Author: Equatable { 38 | static func == (lhs: Author, rhs: Author) -> Bool { 39 | return lhs.id == rhs.id 40 | } 41 | } 42 | 43 | extension Author { 44 | var firstname: String { 45 | let names = name.split(separator: " ") 46 | 47 | guard let firstname = names.first else { 48 | return name 49 | } 50 | 51 | return String(firstname) 52 | } 53 | 54 | var lastname: String { 55 | let names = name.split(separator: " ") 56 | 57 | guard let firstname = names.last else { 58 | return name 59 | } 60 | 61 | return String(firstname) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Models/Thumbnail.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Thumbnail: Codable { 4 | let url: String 5 | let width: Int 6 | let height: Int 7 | 8 | private enum CodingKeys: String, CodingKey { 9 | case url = "source" 10 | case width 11 | case height 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Models/User.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias Users = [User] 4 | 5 | struct User: Codable, Identifiable { 6 | let id = UUID() 7 | let name: String 8 | let email: String 9 | } 10 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Modules/Authentication/Coordinators/AuthenticationCoordinator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | protocol AuthenticationCoordinatorDelegate: AnyObject { 4 | func authenticationCoordinator(_ coordinator: AuthenticationCoordinator, didFinishAuthentication user: User) 5 | } 6 | 7 | final class AuthenticationCoordinator: Coordinator { 8 | 9 | private lazy var authenticationView: AuthenticationView = { 10 | var authenticationView = AuthenticationView() 11 | authenticationView.delegate = self 12 | 13 | return authenticationView 14 | }() 15 | 16 | weak var delegate: AuthenticationCoordinatorDelegate? 17 | 18 | override init() { 19 | super.init() 20 | } 21 | 22 | override func start() -> AnyView { 23 | return authenticationView.any 24 | } 25 | } 26 | 27 | extension AuthenticationCoordinator: AuthenticationViewDelegate { 28 | func authenticationView(_ view: AuthenticationView, didFinishAuthentication user: User) { 29 | delegate?.authenticationCoordinator(self, didFinishAuthentication: user) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Modules/Authentication/ViewModels/UserListViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | protocol AuthenticationViewDelegate: AnyObject { 4 | func authenticationViewDidFinish(_ view: AuthenticationView) 5 | } 6 | 7 | struct AuthenticationView: View { 8 | weak var delegate: AuthenticationViewDelegate? = nil 9 | 10 | var body: some View { 11 | VStack { 12 | Text("Authentication") 13 | 14 | Button(action: { self.delegate?.authenticationViewDidFinish(self) }) { 15 | Text("Login") 16 | } 17 | } 18 | } 19 | } 20 | 21 | // MARK: - Dummy 22 | 23 | #if DEBUG 24 | 25 | final class AuthenticationViewDelegateMock: AuthenticationViewDelegate { 26 | private var isFinished = false 27 | 28 | func authenticationViewDidFinish(_ view: AuthenticationView) { 29 | isFinished = true 30 | } 31 | } 32 | 33 | struct AuthenticationView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | var authenticationView = AuthenticationView() 36 | let delegateMock = AuthenticationViewDelegateMock() 37 | 38 | authenticationView.delegate = delegateMock 39 | 40 | return authenticationView 41 | } 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Modules/Authentication/Views/AuthenticationView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | protocol AuthenticationViewDelegate: AnyObject { 4 | func authenticationView(_ view: AuthenticationView, didFinishAuthentication user: User) 5 | } 6 | 7 | struct AuthenticationView: View { 8 | @State 9 | private var emailValue = "" 10 | 11 | @State 12 | private var passwordValue = "" 13 | 14 | @State 15 | private var saveLogin = true 16 | 17 | weak var delegate: AuthenticationViewDelegate? = nil 18 | 19 | var body: some View { 20 | VStack { 21 | Text("Authentication") 22 | 23 | TextField("Email", text: $emailValue) 24 | .textContentType(.emailAddress) 25 | .frame(height: 30) 26 | 27 | SecureField("Password", text: $passwordValue) 28 | .textContentType(.password) 29 | 30 | Toggle("Save login", isOn: $saveLogin) 31 | 32 | Button(action: { 33 | let user = Users.makeDummy[0] 34 | self.delegate?.authenticationView(self, didFinishAuthentication: user) 35 | 36 | }) { 37 | Text("Login") 38 | } 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Dummy 44 | 45 | #if DEBUG 46 | 47 | final class AuthenticationViewDelegateMock: AuthenticationViewDelegate { 48 | private var isFinished = false 49 | 50 | func authenticationView(_ view: AuthenticationView, didFinishAuthentication user: User) { 51 | isFinished = true 52 | } 53 | } 54 | 55 | struct AuthenticationView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | var authenticationView = AuthenticationView() 58 | let delegateMock = AuthenticationViewDelegateMock() 59 | 60 | authenticationView.delegate = delegateMock 61 | 62 | return authenticationView 63 | } 64 | } 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /CoordinatorSwiftUI WatchKit Extension/SourceCode/Modules/Authors/Coordinators/AuthorsCoordinator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // MARK: - Coordinator Delegate Protocol 4 | 5 | protocol AuthorsCoordinatorDelegate: AnyObject { 6 | func authorsCoordinatorDidFinish(_ coordinator: AuthorsCoordinator) 7 | } 8 | 9 | // MARK: - Coordinator 10 | 11 | final class AuthorsCoordinator: Coordinator { 12 | @Published 13 | private var authorsStore = AuthorsStore() 14 | 15 | private lazy var authorsListView: AuthorsListView = { 16 | let authorsProvider = AuthorsProvider() 17 | 18 | let authorsListViewModel = AuthorsListViewModel( 19 | provider: authorsProvider, 20 | store: authorsStore 21 | ) 22 | 23 | var authorsListView = AuthorsListView( 24 | viewModel: authorsListViewModel 25 | ) 26 | 27 | authorsListView.delegate = self 28 | 29 | return authorsListView 30 | }() 31 | 32 | private func authorProfileDetailView(for author: Author) -> AuthorProfileDetailView { 33 | let profileDetailViewModel = AuthorProfileDetailViewModel( 34 | author: author, 35 | store: authorsStore 36 | ) 37 | 38 | let authorProfileDetailView = AuthorProfileDetailView( 39 | viewModel: profileDetailViewModel 40 | ) 41 | 42 | return authorProfileDetailView 43 | } 44 | 45 | weak var delegate: AuthorsCoordinatorDelegate? 46 | 47 | // MARK: - Initialisation 48 | 49 | override init() { 50 | super.init() 51 | } 52 | 53 | override func start() -> AnyView { 54 | return authorsListView 55 | // This is a workaround. It is here to make possible rebuild AuthorsListView every time AuthorsStore instance is updated 56 | .environmentObject(authorsStore) 57 | .any 58 | } 59 | } 60 | 61 | // MARK: - AuthorsListView Delegate 62 | 63 | extension AuthorsCoordinator: AuthorsListViewDelegate { 64 | 65 | func authorsListViewDidFinishSession(_ view: AuthorsListView) { 66 | delegate?.authorsCoordinatorDidFinish(self) 67 | } 68 | 69 | func authorsListView