├── .gitignore ├── README.md ├── Tuist ├── Config.swift ├── Dependencies.swift ├── ProjectDescriptionHelpers │ ├── Project+Env.swift │ ├── Project+InfoPlist.swift │ ├── Project+Templates.swift │ └── Project+uFeature.swift └── Templates │ └── framework │ ├── framework.swift │ └── project.stencil ├── Workspace.swift └── modules ├── App ├── Derived │ └── InfoPlists │ │ └── ExampleApp-Info.plist ├── Project.swift └── src │ ├── App.swift │ └── MainView.swift ├── Common ├── Derived │ └── InfoPlists │ │ └── Common-Info.plist ├── Project.swift └── src │ └── Common.swift ├── RandomProvider ├── Derived │ └── InfoPlists │ │ ├── RandomProvider-Info.plist │ │ └── RandomProviderInterface-Info.plist ├── Project.swift ├── interface │ └── NumberProvider.swift └── src │ ├── NumberProviderRandom.swift │ └── NumberProviderZero.swift └── RandomScreen ├── Derived └── InfoPlists │ ├── RandomScreen-Info.plist │ └── RandomScreenInterface-Info.plist ├── Project.swift ├── interface └── RandomScreen.swift └── src ├── RandomScreenBig.swift └── RandomScreenSimple.swift /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcworkspace 2 | *.xcodeproj 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TuistExample 2 | 3 | Example for using Tuist with modular app architecture. Check out the articles! 4 | 5 | - [iOS App As a Microservice. Modularize Your App With Tuist](https://alexdremov.me/ios-app-as-a-microservice-modularize-your-app-with-tuist/) 6 | - [iOS App As a Microservice. Build Robust App Architecture](https://alexdremov.me/ios-app-as-a-microservice-build-robust-app-architecture/) 7 | 8 | ## Graph 9 | ![graph](https://user-images.githubusercontent.com/25539425/194518990-de3570df-f243-4571-b4e5-5db30637e5f6.svg) 10 | 11 | ## Visuals 12 | 13 | https://user-images.githubusercontent.com/25539425/194519593-14910e2a-aef3-453a-9741-055ac1d6fde4.mp4 14 | -------------------------------------------------------------------------------- /Tuist/Config.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let config = Config( 4 | plugins: [ 5 | .git(url: "https://github.com/tuist/tuist-plugin-lint", tag: "0.3.0") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /Tuist/Dependencies.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let spmDeps = SwiftPackageManagerDependencies( 5 | [ 6 | .remote(url: "https://github.com/AlexRoar/FoggyColors", 7 | requirement: .branch("main")), 8 | .remote(url: "https://github.com/apple/swift-async-algorithms", 9 | requirement: .branch("main")) 10 | ], 11 | productTypes: [ 12 | "FoggyColors": ProjectDescriptionHelpers.defaultPackageType, 13 | "AsyncAlgorithms": ProjectDescriptionHelpers.defaultPackageType 14 | ] 15 | ) 16 | 17 | let dependencies = Dependencies( 18 | swiftPackageManager: spmDeps, 19 | platforms: [ 20 | .iOS 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Project+Env.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public enum BuildType { 4 | case debug 5 | case release 6 | } 7 | 8 | public let buildType: BuildType = { 9 | Environment.buildTypeRelease.getBoolean(default: false) ? .release : .debug 10 | }() 11 | 12 | public var defaultPackageType: ProjectDescription.Product = { 13 | buildType == .release ? .staticFramework : .framework 14 | }() 15 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Project+InfoPlist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project+InfoPlist.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by Alex Dremov on 23.08.2022. 6 | // 7 | 8 | import ProjectDescription 9 | 10 | public let infoPlistExtension: [_: InfoPlist.Value] = [ 11 | "CFBundleShortVersionString": "1.0", 12 | "CFBundleVersion": "1", 13 | "UIMainStoryboardFile": "", 14 | "UILaunchStoryboardName": "", 15 | "NSHealthShareUsageDescription": 16 | .string("Your health and workout records will be used localy for visualizing and analyzing your data"), 17 | "NSHealthUpdateUsageDescription": 18 | .string("Your health and workout records will be used localy for visualizing and analyzing your data"), 19 | "NSHealthRecordsUsageDescription": 20 | .string("Your health and workout records will be used localy for visualizing and analyzing your data") 21 | ] 22 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Project+Templates.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let rootPackagesName = "com.exampleproject." 4 | 5 | private func makeBundleID(with addition: String) -> String { 6 | (rootPackagesName + addition).lowercased() 7 | } 8 | 9 | public extension Target { 10 | static func makeApp( 11 | name: String, 12 | sources: ProjectDescription.SourceFilesList, 13 | dependencies: [ProjectDescription.TargetDependency] 14 | ) -> Target { 15 | Target( 16 | name: name, 17 | platform: .iOS, 18 | product: .app, 19 | bundleId: makeBundleID(with: "app"), 20 | deploymentTarget: .iOS(targetVersion: "16.0", devices: .iphone), 21 | infoPlist: .extendingDefault(with: infoPlistExtension), 22 | sources: sources, 23 | dependencies: dependencies 24 | ) 25 | } 26 | 27 | static func makeFramework( 28 | name: String, 29 | sources: ProjectDescription.SourceFilesList, 30 | dependencies: [ProjectDescription.TargetDependency] = [], 31 | resources: ProjectDescription.ResourceFileElements? = [] 32 | ) -> Target { 33 | Target( 34 | name: name, 35 | platform: .iOS, 36 | product: defaultPackageType, 37 | bundleId: makeBundleID(with: name + ".framework"), 38 | sources: sources, 39 | resources: resources, 40 | dependencies: dependencies 41 | ) 42 | } 43 | 44 | private static func feature( 45 | implementation featureName: String, 46 | dependencies: [ProjectDescription.TargetDependency] = [], 47 | resources: ProjectDescription.ResourceFileElements? = [] 48 | ) -> Target { 49 | .makeFramework( 50 | name: featureName, 51 | sources: [ "src/**" ], 52 | dependencies: dependencies, 53 | resources: resources 54 | ) 55 | } 56 | 57 | private static func feature( 58 | interface featureName: String, 59 | dependencies: [ProjectDescription.TargetDependency] = [], 60 | resources: ProjectDescription.ResourceFileElements? = [] 61 | ) -> Target { 62 | .makeFramework( 63 | name: featureName + "Interface", 64 | sources: [ "interface/**" ], 65 | dependencies: dependencies, 66 | resources: resources 67 | ) 68 | } 69 | 70 | static func feature( 71 | implementation featureName: Feature, 72 | dependencies: [ProjectDescription.TargetDependency] = [], 73 | resources: ProjectDescription.ResourceFileElements? = [] 74 | ) -> Target { 75 | .feature( 76 | implementation: featureName.rawValue, 77 | dependencies: dependencies, 78 | resources: resources 79 | ) 80 | } 81 | 82 | static func feature( 83 | interface featureName: Feature, 84 | dependencies: [ProjectDescription.TargetDependency] = [], 85 | resources: ProjectDescription.ResourceFileElements? = [] 86 | ) -> Target { 87 | .feature( 88 | interface: featureName.rawValue, 89 | dependencies: dependencies, 90 | resources: resources 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Project+uFeature.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public enum Feature: String { 4 | case Common = "Common" 5 | case RandomProvider = "RandomProvider" 6 | case RandomScreen = "RandomScreen" 7 | case App = "App" 8 | } 9 | 10 | public enum External: String { 11 | case FoggyColors = "FoggyColors" 12 | case AsyncAlgorithms = "AsyncAlgorithms" 13 | } 14 | 15 | public extension ProjectDescription.TargetDependency { 16 | private static func feature(target: String, featureName: String) -> ProjectDescription.TargetDependency { 17 | .project(target: target, path: .relativeToRoot("modules/" + featureName)) 18 | } 19 | 20 | private static func feature(interface moduleName: String) -> ProjectDescription.TargetDependency { 21 | .feature(target: moduleName + "Interface", featureName: moduleName) 22 | } 23 | 24 | private static func feature(implementation moduleName: String) -> ProjectDescription.TargetDependency { 25 | .feature(target: moduleName, featureName: moduleName) 26 | } 27 | 28 | static func feature(interface moduleName: Feature) -> ProjectDescription.TargetDependency { 29 | .feature(interface: moduleName.rawValue) 30 | } 31 | 32 | static func feature(implementation moduleName: Feature) -> ProjectDescription.TargetDependency { 33 | .feature(implementation: moduleName.rawValue) 34 | } 35 | 36 | static func external(_ module: External) -> ProjectDescription.TargetDependency { 37 | .external(name: module.rawValue) 38 | } 39 | 40 | static var common: ProjectDescription.TargetDependency { 41 | .feature(implementation: .Common) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tuist/Templates/framework/framework.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let nameAttribute: Template.Attribute = .required("name") 4 | 5 | 6 | let template = Template( 7 | description: "Framework template", 8 | attributes: [ 9 | nameAttribute 10 | ], items: [ 11 | .file( 12 | path: "modules/\(nameAttribute)/Project.swift", 13 | templatePath: "project.stencil" 14 | ), 15 | .string(path: "modules/\(nameAttribute)/src/implementation.swift", contents: "// Module \(nameAttribute)\nimport Foundation"), 16 | .string(path: "modules/\(nameAttribute)/interface/interface.swift", contents: "// Module \(nameAttribute)\nimport Foundation") 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Tuist/Templates/framework/project.stencil: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project( 5 | name: Feature.{{name}}.rawValue, 6 | targets: [ 7 | .feature( 8 | interface: .{{name}}, 9 | dependencies: [ 10 | .common 11 | ] 12 | ), 13 | .feature( 14 | implementation: .{{name}}, 15 | dependencies: [ 16 | .common, 17 | .feature(interface: .{{name}}), 18 | ], 19 | resources: [ 20 | "resources/**" 21 | ] 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Workspace.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let workspace = Workspace( 4 | name: "ExampleWorkspace", 5 | projects: [ 6 | "modules/*" 7 | ] 8 | ) 9 | -------------------------------------------------------------------------------- /modules/App/Derived/InfoPlists/ExampleApp-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSHealthRecordsUsageDescription 24 | Your health and workout records will be used localy for visualizing and analyzing your data 25 | NSHealthShareUsageDescription 26 | Your health and workout records will be used localy for visualizing and analyzing your data 27 | NSHealthUpdateUsageDescription 28 | Your health and workout records will be used localy for visualizing and analyzing your data 29 | UILaunchStoryboardName 30 | 31 | UIMainStoryboardFile 32 | 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /modules/App/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project( 5 | name: Feature.App.rawValue, 6 | targets: [ 7 | .makeApp( 8 | name: "ExampleApp", 9 | sources: [ 10 | "src/**" 11 | ], 12 | dependencies: [ 13 | .common, 14 | .feature(implementation: .RandomProvider), 15 | .feature(interface: .RandomProvider), 16 | 17 | .feature(implementation: .RandomScreen), 18 | .feature(interface: .RandomScreen) 19 | ] 20 | ) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /modules/App/src/App.swift: -------------------------------------------------------------------------------- 1 | // Module App 2 | import Foundation 3 | import SwiftUI 4 | 5 | @main 6 | struct ExampleApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | MainView() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/App/src/MainView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | import RandomProvider 5 | import RandomProviderInterface 6 | import RandomScreen 7 | import RandomScreenInterface 8 | 9 | import Common 10 | 11 | struct MainView: View { 12 | enum RandomMode: String, CaseIterable, Identifiable { 13 | case random, zero 14 | var id: Self { self } 15 | } 16 | 17 | enum ScreenMode: String, CaseIterable, Identifiable { 18 | case big, simple 19 | var id: Self { self } 20 | } 21 | 22 | @State private var screen: ScreenMode = .simple 23 | @State private var random: RandomMode = .random 24 | 25 | private var randomRange: ClosedRange { 26 | random == .random ? 0...100 : 0...0 27 | } 28 | 29 | var body: some View { 30 | VStack { 31 | VStack { 32 | Picker("Screen", selection: $screen) { 33 | ForEach(ScreenMode.allCases) { flavor in 34 | Text(flavor.rawValue.capitalized) 35 | } 36 | } 37 | Picker("Random", selection: $random) { 38 | ForEach(RandomMode.allCases) { topping in 39 | Text(topping.rawValue.capitalized) 40 | } 41 | } 42 | } 43 | .pickerStyle(.segmented) 44 | Spacer() 45 | relevantScreen 46 | Spacer() 47 | } 48 | .padding() 49 | } 50 | 51 | @ViewBuilder 52 | private var relevantScreen: some View { 53 | switch screen { 54 | case .simple: 55 | simpleScreen 56 | case .big: 57 | bigScreen 58 | } 59 | } 60 | 61 | private var relevantRandomProvider: NumberProvider { 62 | switch random { 63 | case .random: 64 | return NumberProviderRandom(range: randomRange) 65 | case .zero: 66 | return NumberProviderZero() 67 | } 68 | } 69 | 70 | @ViewBuilder 71 | private var simpleScreen: some View { 72 | HStack { 73 | RandomScreenSimple(randomProvider: relevantRandomProvider) 74 | } 75 | } 76 | 77 | @ViewBuilder 78 | private var bigScreen: some View { 79 | HStack { 80 | Text("\(randomRange.lowerBound)").gray 81 | RandomScreenBig(randomProvider: relevantRandomProvider) 82 | .frame(minWidth: 200) 83 | Text("\(randomRange.upperBound)").gray 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /modules/Common/Derived/InfoPlists/Common-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /modules/Common/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project( 5 | name: Feature.Common.rawValue, 6 | targets: [ 7 | .feature( 8 | implementation: .Common 9 | ) 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /modules/Common/src/Common.swift: -------------------------------------------------------------------------------- 1 | // Coomon.swift 2 | import SwiftUI 3 | 4 | public extension Text { 5 | var gray: some View { 6 | self.foregroundColor(.gray) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/RandomProvider/Derived/InfoPlists/RandomProvider-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /modules/RandomProvider/Derived/InfoPlists/RandomProviderInterface-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /modules/RandomProvider/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project( 5 | name: Feature.RandomProvider.rawValue, 6 | targets: [ 7 | .feature( 8 | interface: .RandomProvider, 9 | dependencies: [ 10 | .common 11 | ] 12 | ), 13 | .feature( 14 | implementation: .RandomProvider, 15 | dependencies: [ 16 | .common, 17 | .feature(interface: .RandomProvider), 18 | ] 19 | ) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /modules/RandomProvider/interface/NumberProvider.swift: -------------------------------------------------------------------------------- 1 | // Module Biz 2 | import Foundation 3 | 4 | public protocol NumberProvider { 5 | var number: Int { get } 6 | } 7 | -------------------------------------------------------------------------------- /modules/RandomProvider/src/NumberProviderRandom.swift: -------------------------------------------------------------------------------- 1 | // Module Biz 2 | import Foundation 3 | import RandomProviderInterface 4 | 5 | public struct NumberProviderRandom: NumberProvider { 6 | private let range: ClosedRange 7 | 8 | public var number: Int { 9 | Int.random(in: range) 10 | } 11 | 12 | public init(range: ClosedRange) { 13 | self.range = range 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/RandomProvider/src/NumberProviderZero.swift: -------------------------------------------------------------------------------- 1 | // Module Biz 2 | import Foundation 3 | import RandomProviderInterface 4 | 5 | public struct NumberProviderZero: NumberProvider { 6 | public let number = 0 7 | 8 | public init() { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/RandomScreen/Derived/InfoPlists/RandomScreen-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /modules/RandomScreen/Derived/InfoPlists/RandomScreenInterface-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /modules/RandomScreen/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project( 5 | name: Feature.RandomScreen.rawValue, 6 | targets: [ 7 | .feature( 8 | interface: .RandomScreen, 9 | dependencies: [ 10 | .common 11 | ] 12 | ), 13 | .feature( 14 | implementation: .RandomScreen, 15 | dependencies: [ 16 | .common, 17 | .feature(interface: .RandomScreen), 18 | .feature(interface: .RandomProvider), 19 | ] 20 | ) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /modules/RandomScreen/interface/RandomScreen.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | public protocol RandomScreen: View { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /modules/RandomScreen/src/RandomScreenBig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import RandomScreenInterface 4 | import RandomProviderInterface 5 | 6 | 7 | public struct RandomScreenBig: RandomScreen { 8 | let randomProvider: NumberProvider 9 | 10 | @State var number: Int = 0 11 | 12 | public init(randomProvider: NumberProvider) { 13 | self.randomProvider = randomProvider 14 | } 15 | 16 | public var body: some View { 17 | VStack { 18 | Text("\(number)") 19 | .font(.system(size: 80)) 20 | Button("generate") { 21 | number = randomProvider.number 22 | } 23 | }.onAppear { 24 | number = randomProvider.number 25 | } 26 | .animation(.default, value: number) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/RandomScreen/src/RandomScreenSimple.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import RandomScreenInterface 4 | import RandomProviderInterface 5 | 6 | 7 | public struct RandomScreenSimple: RandomScreen { 8 | let randomProvider: NumberProvider 9 | 10 | @State var number: Int = 0 11 | 12 | public init(randomProvider: NumberProvider) { 13 | self.randomProvider = randomProvider 14 | } 15 | 16 | public var body: some View { 17 | VStack { 18 | Text("\(number)") 19 | Button("generate") { 20 | number = randomProvider.number 21 | } 22 | }.onAppear { 23 | number = randomProvider.number 24 | } 25 | .animation(.default, value: number) 26 | } 27 | } 28 | --------------------------------------------------------------------------------