diff --git a/02_Swift_interview_questions/practical.md b/02_Swift_interview_questions/practical.md index aeb7e95..e79050e 100755 --- a/02_Swift_interview_questions/practical.md +++ b/02_Swift_interview_questions/practical.md @@ -84,6 +84,22 @@ Swift面试题共分为两篇: ![SwiftUI_and_Combine](../assets/SwiftUI_and_Combine.png) +However, from iOS17 things about Combine change a lot. + +You should always consult the developers’ documentation to find out about the most recent documentation: https://developer.apple.com/documentation/swiftui/. + +The Combine framework with this new update is going to be substituted with the Observation framework, which extends State and Environment to replace StateObject and EnvironmentObject. The net effect is a huge simplification. + +You can introduce the new changes in your existing apps that use Combine incrementally, keeping the old approaches in place and substituting them as you progress; much of what you know already and is available in previous versions of SwiftUI will remain valid for quite a while. It normally takes a couple of years for the old approach to be completely replaced. But keep in mind that Combine is practically going to die eventually. And with Combine, the concept of Apple supporting reactive frameworks natively will also die. + +So, relying on reactive frameworks is probably going to be a bad idea for your architectural choices on Apple systems, as the evolution of the Apple operating system seems to be moving away from that concept. + +Another piece of big news just presented during WWDC2023 is the replacement of the Core Data framework with the new, simpler-to-use SwiftData framework for data persistence. SwiftData is compatible with Core Data to the point that it is possible to use both in the same app. Xcode is able to convert Core Data models into classes for use with SwiftData, so the conversion to the new framework is going to be rather simple. It still makes sense to use Core Data, as this transition will take about two years, and SwiftData is interoperable with Core Data. + +There is, however, going to be less and less reason to depend on third-party frameworks such as Realm. + +From a historical perspective, trying to innovate in front of Apple by introducing change means that you are going to spend quite a lot to keep your applications up to spec and maintain them. All technical engineering choices are economic choices, and trying to guess the future is part of the business. + ## What’s the difference between SwiftUI and UIKit Lifecycle Methods? ![SwiftUI_and_Combine](../assets/SwiftUI_vs_UIKit_Lifecycle_Methods_Difference.jpg) @@ -92,13 +108,21 @@ Swift面试题共分为两篇: https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject -我写的有点老了,现在iOS17 是Observation框架,性能更好了,不过写法不同了. 等忙完这段时间, 我完善下. 先看下iOS17 以前的写法. [SwiftUI Data Flow 2023]( https://troz.net/post/2023/swiftui-data-flow-2023/ "") +iOS17 以后 是Observation框架,性能更好了,不过写法不同了. [SwiftUI Data Flow 2023]( https://troz.net/post/2023/swiftui-data-flow-2023/ "") [Answer from AI](https://chat.openai.com/share/cd46dff5-9d7e-4990-8fe9-595e2aa65080) -![](../assets/Property_Wrapper_in_SwiftUI.jpg) +![](../assets/Property_Wrapper_in_SwiftUI/ios16_overview.png) + +![](../assets/Property_Wrapper_in_SwiftUI/ios17_overview.png) + +![](../assets/Property_Wrapper_in_SwiftUI/ios16_code.png) +![](../assets/Property_Wrapper_in_SwiftUI/ios17_code.png) +![](../assets/Property_Wrapper_in_SwiftUI/ios16_pros_cons.png) + +![](../assets/Property_Wrapper_in_SwiftUI/ios17_pros_cons.png) ![](../assets/16912073172453.jpg) @@ -239,13 +263,16 @@ As for why one might prefer one pattern over the other, it often depends on spec What is the difference between clean and MVVM architecture? -![](../assets/MVVM_Clean_SOLID/banner_en.jpg) +![](../assets/MVVM_Clean_SOLID/banner_en.png) + +![](../assets/MVVM_Clean_SOLID/en.png) -![](../assets/MVVM_Clean_SOLID/en.jpg) +![](../assets/MVVM_Clean_SOLID/banner_cn.png) -![](../assets/MVVM_Clean_SOLID/banner_cn.jpg) +![](../assets/MVVM_Clean_SOLID/cn.png) +![](../assets/MVVM_Clean_SOLID/tca_onion_layering.png) +![](../assets/MVVM_Clean_SOLID/tca_horizontal_layer.png) -![](../assets/MVVM_Clean_SOLID/cn.jpg) ## Clean MVVM 框架 [https://github.com/kudoleh/iOS-Clean-Architecture-MVVM](https://github.com/kudoleh/iOS-Clean-Architecture-MVVM) @@ -281,9 +308,9 @@ TCA 虽然是一种设计模式, 但也有一个非常流行的最佳实践库, state,action本身内部内聚很高, 类似声明. 一旦类似delegate的action出去了. 单看一个模块就读不出完整逻辑了. 我觉得reducer做到了有所有的逻辑啊,delegate也只是一种effect而已. 跟其他的effect没有什么区别. 原来这个模块这条链路是这个意思.它问了Clean框架, 我说我听过, 没用过. 新西兰有很多移动端的架构就是Clean. -![](../assets/tca_architecture_design_pattern/tca_architecture_design_pattern_qa.jpg) +![](../assets/tca_architecture_design_pattern/tca_architecture_design_pattern_qa.png) -![](../assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.jpg) +![](../assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.png) [Composable Architecture]( https://zenn.dev/inamiy/books/3dd014a50f321040a047/viewer/cca752a1fe8700f9d0c0 "") @@ -490,7 +517,17 @@ Modern MVVM: https://www.vadimbulavin.com/modern-mvvm-ios-app-architecture-with- **Redux**: + +Please look at the demo named SwiftUIRedux in this repository to learn more about redux with SwiftUI. + Redux: https://www.raywenderlich.com/22096649-getting-a-redux-vibe-into-swiftui +![](../assets/redux_architecture_design_pattern/redux_architecture_design_pattern_QA.png) + + +![](../assets/redux_architecture_design_pattern/redux_architecture_design_pattern_en.png) + +![](../assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy1.png) +![](../assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy2.png) #### 2. Describe binding in MVVM. @@ -1080,8 +1117,13 @@ When using such services, you generally need to send the device token to your ow obtain | = have = 获得. subsequent | = following = 随后的. + + iOS 面试问题: 你能解释一下基于令牌的身份验证是如何工作的吗?如何在这种身份验证中,从应用程序接口获取一个令牌,并在随后的应用程序接口调用中使用它? +token-based authentication == JWT. +该问题主要是考察JWT工作原理. + [Answer from AI](https://chat.openai.com/share/cd8cfa69-ab4c-4cf3-b45d-45884b52a72c) 令牌基础认证是一种用于验证用户身份并授权他们访问系统或应用的安全技术。以下是其基本运作过程: diff --git a/Behavioral_based_interviewing_Competency_Based/objective_qa.md b/Behavioral_based_interviewing_Competency_Based/objective_qa.md index a6ead28..8ce4976 100755 --- a/Behavioral_based_interviewing_Competency_Based/objective_qa.md +++ b/Behavioral_based_interviewing_Competency_Based/objective_qa.md @@ -120,7 +120,7 @@ Can you go into more detail about what you did With backend development? Generally speaking, I was working as an iOS developer. However, during my work, I have to perform as a backend developer to develop the API, which is short for an application programming interface. -In addition, I’d like to introduce my primary role as an iPhone platform developer. I developed some trendy applications in the app store, like the Alibaba cloud application and some popular frameworks. I used to use some programming languages like react-native and objective-c, swift. But, I still want to go further. +In addition, I’d like to introduce my primary role as an iPhone platform developer. I developed some trendy applications in the app store, like the XXX application and some popular frameworks. I used to use some programming languages like react-native and objective-c, swift. But, I still want to go further. ## What are your salary requirements? @@ -146,7 +146,7 @@ If I had time, I'd like to post blogs on the internet, share my experience with What is the first thing you will do after working in New Zealand as a software engineer? How are you going to settle well? -Before I answer this question, I'd like to share my experience. I had been to New Zealand three years ago. When I married and had a honeymoon with my wife, this was my second time in New Zealand. +Before I answer this question, I'd like to share my experience. I had been to New Zealand three years ago. When I xxx, this was my second time in New Zealand. So, how can I adapt to life in New Zealand? diff --git a/Behavioral_based_interviewing_Competency_Based/technical_qa.md b/Behavioral_based_interviewing_Competency_Based/technical_qa.md index 6097b3c..60f94f2 100755 --- a/Behavioral_based_interviewing_Competency_Based/technical_qa.md +++ b/Behavioral_based_interviewing_Competency_Based/technical_qa.md @@ -22,6 +22,88 @@ ![12](../assets/solid/12.jpg) ![13](../assets/solid/13.jpg) + + + +### S – Single Responsibility Principle (SRP) + +**Concept**: A class should have only one reason to exist. + +**Application**: Each class in your application should have only one specific job or responsibility. For instance, if you have a `UserHandler` class, its responsibility should be strictly limited to user-related operations, such as creating, updating, or deleting users, and not also include network functionality, for example. + +In Swift or SwiftUI, adhering to the SRP means ensuring that each class or module is focused on a single task. This approach simplifies maintenance and reduces the likelihood of errors. For example, you might have: + +- **A class for handling network requests**: This class is responsible solely for network communication. It could include methods for sending HTTP requests, handling responses, and managing errors related to networking. + + ```swift + class NetworkManager { + func fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) { + // Implementation for network request + } + } + ``` + +- **A class for parsing data**: This class handles the parsing of raw data into usable objects or structures. It does not concern itself with how the data was obtained. + + ```swift + class DataParser { + func parse(data: Data) -> ParsedObject? { + // Implementation for parsing data + } + } + ``` + +- **A class for managing the user interface**: This class or module focuses on rendering the UI and handling user interactions. + + ```swift + struct ContentView: View { + var body: some View { + // Implementation for user interface + } + } + ``` + +By keeping these responsibilities separate, each class or module remains clean, focused, and easy to understand. This separation of concerns aligns with the Single Responsibility Principle and promotes a more modular and maintainable codebase. + +**Example in Swift**: + +Consider the following Swift classes and their responsibilities: + +```swift +class NetworkManager { + func fetchUserData(completion: @escaping (Data?, Error?) -> Void) { + // Code to fetch user data from network + } +} + +class UserParser { + func parseUser(data: Data) -> User? { + // Code to parse data into User object + } +} + +struct UserView: View { + var user: User + + var body: some View { + VStack { + Text(user.name) + Text(user.email) + } + } +} +``` + +In this example: +- `NetworkManager` is responsible only for fetching data from the network. +- `UserParser` is responsible only for parsing that data into a `User` object. +- `UserView` is responsible only for displaying the user information in the UI. + +Following SRP, these classes and structs are easier to test, debug, and maintain because each has a single responsibility. + +In summary, the Single Responsibility Principle encourages developers to create classes and modules that focus on one particular task or responsibility, leading to a more organized and manageable code structure. + + ### STAR answer format STAR stands for: diff --git a/Swift/Swift101/Swift101Tests/SwiftPlaygroundTest.swift b/Swift/Swift101/Swift101Tests/SwiftPlaygroundTest.swift index d3b621b..d0a5fd9 100644 --- a/Swift/Swift101/Swift101Tests/SwiftPlaygroundTest.swift +++ b/Swift/Swift101/Swift101Tests/SwiftPlaygroundTest.swift @@ -37,6 +37,105 @@ final class SwiftPlaygroundTest: XCTestCase { // // Put the code you want to measure the time of here. // } // } + func welcome() -> String { + return "welcome!" + } + + func testWelcome() { + let myWelcome = welcome() + + print(myWelcome + "Elon!!") + + let customWelcome = welcome(name: "Elon!!!") + print(customWelcome) + } + + func welcome(name: String) -> String { + return "Welcome, \(name)" + } + + func error() -> (code: Int, description: String) { + return (404, "Not found") + } + + func testError() { + let myError = error() + print("Error code \(myError.code): \(myError.description)") + } + func subtractOne(from number: Int) -> Int { + return number - 1 + } + + func subtractOne(_ number: Int) -> Int { + return number - 1 + } + func subtractOne(number: Int) -> Int { + return number - 1 + } + + func testSubtract() { + print(subtractOne(from: 5)) + print(subtractOne(5)) + print(subtractOne(number: 5)) + } + + func addOne(to number: Int = 0) -> Int { + return number + 1 + } + + func testAddone() { + print(addOne()) + print(addOne(to: 5)) + } + + + enum Product: CaseIterable { + case laptop + case desktop + case phone + case watch + } + enum Suit: String { + case spades = "♠️" + case hearts = "♥️" + case diamonds = "♦️" + case clubs = "♣️" + } + + enum Card { + case regular(Int, Suit) + case joker + } + + func testEnum() { + let product = Product.laptop + var result: String + switch product { + case .laptop: + result = "laptop" + case .desktop: + result = "desktop" + case .phone: + result = "phone" + case .watch: + result = "watch" + } + + print(result) + print(Product.allCases) + + print(Suit.spades.rawValue) + + let card = Card.regular(7, .hearts) + switch card { + case .regular(let number, let suit): + result = "The \(number) of \(suit.rawValue)" + case .joker: + result = "Joker" + } + + print(result) + } func testBasicDataTypes() { let a: UInt8 = 240 @@ -47,6 +146,99 @@ final class SwiftPlaygroundTest: XCTestCase { print("AudioSample min \(AudioSample.min), AudioSample max \(AudioSample.max), this AudioSample is \(sample)") } + + + struct Point { + var x = 0 + var y = 0 + } + + class Student { + var name = "name" + var grade = 1 + } + + func testStruct() { + var p1 = Point() + var p2 = p1 + p2.x = 4 + p2.y = 5 + + print("p1 is at (\(p1.x), \(p1.y))") + print("p2 is at (\(p2.x), \(p2.y))") + } + + func testClass() { + var s1 = Student() + var s2 = s1 + s2.name = "Elon" + s2.grade = 2 + + print("s1 is \(s1.name) in grade \(s1.grade)") + print("s2 is \(s2.name) in grade \(s2.grade)") + } + + + class Person { + var name: String + + init(name: String) { + self.name = name + } + } + + struct Company { + var size: Int + var manager: Person + + mutating func increaseSize() { + self = Company(size: size + 1, manager: manager) + } + + mutating func increaseSizeV2() { + size += 1 + } + } + + func testStructAndClass() { + var companyA = Company(size: 100, manager: Person(name: "Peter")) + + var companyB = companyA + companyA.size = 150 // if a struct is an immutable value type, why we can mutate the size property? + print(companyA.size) // ? question1 + print(companyB.size) // ? question2 + + companyA.manager.name = "Bob" + print(companyA.manager.name) // ? question3 + print(companyB.manager.name) // ? question4 + + companyA.increaseSize() + print(companyA.size) // ? question5 + companyA.increaseSizeV2() + print(companyA.size) // ? question6 + } + + struct Cube { + var sideLength: Double = 1.0 { + willSet { + print("will set sideLength to \(newValue) with current sideLength \(sideLength)") + } + didSet { + print("did set sideLength to \(sideLength) from oldValue \(oldValue)") + } + } + var volume: Double { + return sideLength * sideLength * sideLength + } + } + + func testStructPropertyObserver() { + var cube = Cube() + print("cube volume is \(cube.volume)") + cube.sideLength = 2.0 + print("cube volume is \(cube.volume)") + } + func testTuple() { let error = (1, "No Autherity") print("error is \(error)") @@ -134,7 +326,7 @@ final class SwiftPlaygroundTest: XCTestCase { 5 """ print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the numbers is? A:\(numbers)") - + numbers = """ 1 @@ -144,7 +336,7 @@ final class SwiftPlaygroundTest: XCTestCase { 5 """ print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the numbers is? A:\(numbers)") - + numbers = """ @@ -155,7 +347,7 @@ final class SwiftPlaygroundTest: XCTestCase { 5 """ print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the numbers is? A:\(numbers)") - + numbers = """ 1 2 @@ -164,19 +356,163 @@ final class SwiftPlaygroundTest: XCTestCase { 5 """ print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the numbers is? A:\(numbers)") - + var strMutiLine = "\n1\n2\n3" print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the strMutiLine is? A:\(strMutiLine)") - + strMutiLine = #"\n1\n2\n3"# print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the strMutiLine is? A:\(strMutiLine)") strMutiLine = ##"\n1\"#n2\n3"## - + print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the strMutiLine is? A:\(strMutiLine)") strMutiLine = ##"\n1\"#n2\##n3"## print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the strMutiLine is? A:\(strMutiLine)") strMutiLine = ##"\n1\"#n2\#n3"## print("🔴 Swift Class Name:\((#file as NSString).lastPathComponent) func Name:\(#function)(at \(#line) line), Q: what the strMutiLine is? A:\(strMutiLine)") } + + + struct BankAccount { + var balance: Double + init(balance: Double) { + self.balance = balance + } + + mutating func deposit(amount: Double) { + balance += amount + } + + mutating func withdraw(amount: Double) -> Double { + let amount = min(balance, amount) + balance -= amount + return amount + } + } + + func testStructMutatingWithBankAccount() { + var account = BankAccount(balance: 100) + account.deposit(amount: 50) + print("account balance is \(account.balance)") + print("account balance is \(account.withdraw(amount: 100))") + print("account balance is \(account.withdraw(amount: 1000))") + } + + enum LightSwitch { + case red, green, yellow + + mutating func next() { + switch self { + case .red: + self = .green + case .green: + self = .yellow + case .yellow: + self = .red + } + } + } + func testMethodWithLightSwitch() { + var lightSwitch = LightSwitch.red + lightSwitch.next() + print("lightSwitch is \(lightSwitch)") + lightSwitch.next() + print("lightSwitch is \(lightSwitch)") + lightSwitch.next() + print("lightSwitch is \(lightSwitch)") + lightSwitch.next() + print("lightSwitch is \(lightSwitch)") + } + + + class Enemy { + var health = 100 + var speed = 1 + func makeAttackNoise() { + print("Enemy attack") + } + + var desription: String { + return "Enemy with health \(health) and speed \(speed)" + } + } + + class Lion: Enemy { + func scratch() { + print("Lion scratch") + } + + override func makeAttackNoise() { + print("Lion attack") + super.makeAttackNoise() + } + override var desription: String { + return "Lion with health \(health) and speed \(speed)" + } + } + + + func testInheritance() { + let lion = Lion() + print(lion.desription) + lion.makeAttackNoise() + lion.scratch() + print(lion.desription) + } + + struct Shoe { + let brand: String + let size: Int + let color: String + init(brand: String) { + self.brand = brand + size = 10 + color = "Red" + } +// init(brand: String, size: Int, color: String) { +// self.brand = brand +// self.size = size +// self.color = color +// } + } + + func testInitializeWithShoe() { +// let shoe = Shoe(brand: "Nike", size: 10, color: "Red") +// print("shoe is \(shoe)") + + let shoe = Shoe(brand: "Nike") + print("shoe is \(shoe)") + + } + + class Products { + let name: String + init(name: String) { + self.name = name + } + } + + class Bottle: Products { + let size: Double + + init(name: String, size: Double) { + self.size = size + super.init(name: name) + } + + convenience override init(name: String) { + self.init(name: name, size: 500) + } + } + + func testInitializeWithProduct() { + + let product = Products(name: "Water") + print("product is \(product)") + let bottle = Bottle(name: "Water", size: 500) + print("bottle is \(bottle)") + + let bottle2 = Bottle(name: "Water") + print("bottle2 is \(bottle2)") + } } diff --git a/SwiftUI/SwiftUI101/SwiftUI101/ContentView.swift b/SwiftUI/SwiftUI101/SwiftUI101/ContentView.swift index 7844cd3..0c1d779 100644 --- a/SwiftUI/SwiftUI101/SwiftUI101/ContentView.swift +++ b/SwiftUI/SwiftUI101/SwiftUI101/ContentView.swift @@ -13,27 +13,27 @@ class UserSettings: ObservableObject { } struct ContentView: View { - @State private var name = "Anonymous" + @State private var name = "[@State]Anonymous" @StateObject private var settings = UserSettings() var body: some View { NavigationView { VStack { // State usage - TextField("Enter your name", text: $name).background(Color.red) + TextField("[@State]Enter your name", text: $name).background(Color.red) Text("Your name is \(name)") // StateObject usage - Text("Your score is \(settings.score)") - Button("Increase Score") { + Text("[@StateObject]Your score is \(settings.score)") + + Button("[@State&@StateObject]Touch to Increase Score") { settings.score += 1 - name = "chenyilong" + String(settings.score) + name = "[@State]chenyilong" + String(settings.score) } - // Navigate to ChildView, passing the settings NavigationLink(destination: ChildView(settings: settings, name: $name)) { - Text("Go to Second View") + Text("[@Binding]Go to ChildView View") } // Display the ScoreView using @EnvironmentObject @@ -59,10 +59,11 @@ struct ChildView: View { var body: some View { VStack { - Text("Score: \(settings.score)") - TextField("Change name", text: $name).background(Color.red) - Button("Increase Score") { + Text("[@Binding]Score: \(settings.score)") + TextField("[@Binding]Change name", text: $name).background(Color.red) + Button("[@Binding]Increase Score") { settings.score += 1 + name = "[@Binding]chenyilong" + String(settings.score) } Button("Dismiss") { self.presentationMode.wrappedValue.dismiss() @@ -76,6 +77,6 @@ struct ScoreView: View { @EnvironmentObject var settings: UserSettings var body: some View { - Text("Your Score in ScoreView: \(settings.score)") + Text("[@EnvironmentObject]Your Score in ScoreView: \(settings.score)") } } diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp.xcodeproj/project.pbxproj b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp.xcodeproj/project.pbxproj new file mode 100755 index 0000000..c921c5e --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp.xcodeproj/project.pbxproj @@ -0,0 +1,404 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 8DF2C67A28A1955500B57214 /* MovieAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C67928A1955500B57214 /* MovieAppApp.swift */; }; + 8DF2C67C28A1955500B57214 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C67B28A1955500B57214 /* ContentView.swift */; }; + 8DF2C67E28A1955800B57214 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DF2C67D28A1955800B57214 /* Assets.xcassets */; }; + 8DF2C68128A1955800B57214 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DF2C68028A1955800B57214 /* Preview Assets.xcassets */; }; + 8DF2C68928A1961500B57214 /* Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C68828A1961500B57214 /* Webservice.swift */; }; + 8DF2C68C28A1963C00B57214 /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C68B28A1963C00B57214 /* Movie.swift */; }; + 8DF2C68E28A196B700B57214 /* MovieResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C68D28A196B700B57214 /* MovieResponse.swift */; }; + 8DF2C69128A1973A00B57214 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C69028A1973A00B57214 /* Store.swift */; }; + 8DF2C69328A1994800B57214 /* MovieDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C69228A1994800B57214 /* MovieDetail.swift */; }; + 8DF2C69528A199AB00B57214 /* MovieDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C69428A199AB00B57214 /* MovieDetailView.swift */; }; + 8DF2C69828A19AF000B57214 /* NewsArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C69728A19AF000B57214 /* NewsArticle.swift */; }; + 8DF2C69A28A19B1A00B57214 /* NewsArticleResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2C69928A19B1A00B57214 /* NewsArticleResponse.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8DF2C67628A1955500B57214 /* MovieApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MovieApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DF2C67928A1955500B57214 /* MovieAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAppApp.swift; sourceTree = ""; }; + 8DF2C67B28A1955500B57214 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8DF2C67D28A1955800B57214 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8DF2C68028A1955800B57214 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8DF2C68828A1961500B57214 /* Webservice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Webservice.swift; sourceTree = ""; }; + 8DF2C68B28A1963C00B57214 /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; + 8DF2C68D28A196B700B57214 /* MovieResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieResponse.swift; sourceTree = ""; }; + 8DF2C69028A1973A00B57214 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 8DF2C69228A1994800B57214 /* MovieDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetail.swift; sourceTree = ""; }; + 8DF2C69428A199AB00B57214 /* MovieDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailView.swift; sourceTree = ""; }; + 8DF2C69628A19A8000B57214 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 8DF2C69728A19AF000B57214 /* NewsArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticle.swift; sourceTree = ""; }; + 8DF2C69928A19B1A00B57214 /* NewsArticleResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticleResponse.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8DF2C67328A1955500B57214 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8DF2C66D28A1955500B57214 = { + isa = PBXGroup; + children = ( + 8DF2C67828A1955500B57214 /* MovieApp */, + 8DF2C67728A1955500B57214 /* Products */, + ); + sourceTree = ""; + }; + 8DF2C67728A1955500B57214 /* Products */ = { + isa = PBXGroup; + children = ( + 8DF2C67628A1955500B57214 /* MovieApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 8DF2C67828A1955500B57214 /* MovieApp */ = { + isa = PBXGroup; + children = ( + 8DF2C69628A19A8000B57214 /* Info.plist */, + 8DF2C68F28A1971A00B57214 /* Stores */, + 8DF2C68A28A1963100B57214 /* Models */, + 8DF2C68728A1960D00B57214 /* Services */, + 8DF2C67928A1955500B57214 /* MovieAppApp.swift */, + 8DF2C67B28A1955500B57214 /* ContentView.swift */, + 8DF2C67D28A1955800B57214 /* Assets.xcassets */, + 8DF2C67F28A1955800B57214 /* Preview Content */, + 8DF2C69428A199AB00B57214 /* MovieDetailView.swift */, + ); + path = MovieApp; + sourceTree = ""; + }; + 8DF2C67F28A1955800B57214 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8DF2C68028A1955800B57214 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8DF2C68728A1960D00B57214 /* Services */ = { + isa = PBXGroup; + children = ( + 8DF2C68828A1961500B57214 /* Webservice.swift */, + ); + path = Services; + sourceTree = ""; + }; + 8DF2C68A28A1963100B57214 /* Models */ = { + isa = PBXGroup; + children = ( + 8DF2C68B28A1963C00B57214 /* Movie.swift */, + 8DF2C68D28A196B700B57214 /* MovieResponse.swift */, + 8DF2C69228A1994800B57214 /* MovieDetail.swift */, + 8DF2C69728A19AF000B57214 /* NewsArticle.swift */, + 8DF2C69928A19B1A00B57214 /* NewsArticleResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + 8DF2C68F28A1971A00B57214 /* Stores */ = { + isa = PBXGroup; + children = ( + 8DF2C69028A1973A00B57214 /* Store.swift */, + ); + path = Stores; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8DF2C67528A1955500B57214 /* MovieApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8DF2C68428A1955800B57214 /* Build configuration list for PBXNativeTarget "MovieApp" */; + buildPhases = ( + 8DF2C67228A1955500B57214 /* Sources */, + 8DF2C67328A1955500B57214 /* Frameworks */, + 8DF2C67428A1955500B57214 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MovieApp; + productName = MovieApp; + productReference = 8DF2C67628A1955500B57214 /* MovieApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8DF2C66E28A1955500B57214 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 8DF2C67528A1955500B57214 = { + CreatedOnToolsVersion = 14.0; + }; + }; + }; + buildConfigurationList = 8DF2C67128A1955500B57214 /* Build configuration list for PBXProject "MovieApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8DF2C66D28A1955500B57214; + productRefGroup = 8DF2C67728A1955500B57214 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DF2C67528A1955500B57214 /* MovieApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8DF2C67428A1955500B57214 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DF2C68128A1955800B57214 /* Preview Assets.xcassets in Resources */, + 8DF2C67E28A1955800B57214 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8DF2C67228A1955500B57214 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DF2C69A28A19B1A00B57214 /* NewsArticleResponse.swift in Sources */, + 8DF2C67C28A1955500B57214 /* ContentView.swift in Sources */, + 8DF2C69328A1994800B57214 /* MovieDetail.swift in Sources */, + 8DF2C68E28A196B700B57214 /* MovieResponse.swift in Sources */, + 8DF2C67A28A1955500B57214 /* MovieAppApp.swift in Sources */, + 8DF2C69828A19AF000B57214 /* NewsArticle.swift in Sources */, + 8DF2C69128A1973A00B57214 /* Store.swift in Sources */, + 8DF2C68C28A1963C00B57214 /* Movie.swift in Sources */, + 8DF2C69528A199AB00B57214 /* MovieDetailView.swift in Sources */, + 8DF2C68928A1961500B57214 /* Webservice.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8DF2C68228A1955800B57214 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8DF2C68328A1955800B57214 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8DF2C68528A1955800B57214 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MovieApp/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MovieApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.MovieApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8DF2C68628A1955800B57214 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MovieApp/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MovieApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.MovieApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8DF2C67128A1955500B57214 /* Build configuration list for PBXProject "MovieApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DF2C68228A1955800B57214 /* Debug */, + 8DF2C68328A1955800B57214 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8DF2C68428A1955800B57214 /* Build configuration list for PBXNativeTarget "MovieApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DF2C68528A1955800B57214 /* Debug */, + 8DF2C68628A1955800B57214 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8DF2C66E28A1955500B57214 /* Project object */; +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100755 index 0000000..eb87897 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..13613e3 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/Contents.json b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/ContentView.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/ContentView.swift new file mode 100755 index 0000000..ecb0367 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/ContentView.swift @@ -0,0 +1,53 @@ +// +// ContentView.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import SwiftUI + +struct ContentView: View { + + @StateObject private var store = Store() + + private func populateMovies() async { + do { + try await store.fetchMovies() + } catch { + print(error.localizedDescription) + } + } + + var body: some View { + + NavigationStack { + List(store.movies) { movie in + NavigationLink(value: movie) { + HStack { + AsyncImage(url: movie.poster) { image in + image.resizable() + .frame(width: 75, height: 75) + } placeholder: { + ProgressView() + } + Text(movie.title) + } + } + + }.navigationDestination(for: Movie.self, destination: { movie in + MovieDetailView(movie: movie) + }) + } + + .task { + await populateMovies() + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Info.plist b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Info.plist new file mode 100755 index 0000000..6a6654d --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/Movie.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/Movie.swift new file mode 100755 index 0000000..2c47a84 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/Movie.swift @@ -0,0 +1,31 @@ +// +// Movie.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +struct Movie: Decodable, Identifiable, Hashable { + + let imdbId: String + let title: String + let poster: URL + + var id: String { + imdbId + } + + private enum CodingKeys: String, CodingKey { + case imdbId = "imdbID" + case title = "Title" + case poster = "Poster" + } +} + +extension Movie { + static var preview: Movie { + Movie(imdbId: "tt3896198", title: "Guardians of the Galaxy Vol. 2", poster: URL(string: "https://m.media-amazon.com/images/M/MV5BNjM0NTc0NzItM2FlYS00YzEwLWE0YmUtNTA2ZWIzODc2OTgxXkEyXkFqcGdeQXVyNTgwNzIyNzg@._V1_SX300.jpg")!) + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/MovieDetail.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/MovieDetail.swift new file mode 100755 index 0000000..5062579 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/MovieDetail.swift @@ -0,0 +1,28 @@ +// +// MovieDetail.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +struct MovieDetail: Decodable, Identifiable { + + let imdbId: String + let title: String + let poster: URL + let director: String + + var id: String { + imdbId + } + + private enum CodingKeys: String, CodingKey { + case imdbId = "imdbID" + case title = "Title" + case poster = "Poster" + case director = "Director" + } + +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/MovieResponse.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/MovieResponse.swift new file mode 100755 index 0000000..b118fa0 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/MovieResponse.swift @@ -0,0 +1,16 @@ +// +// MovieResponse.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +struct MovieResponse: Decodable { + let search: [Movie] + + private enum CodingKeys: String, CodingKey { + case search = "Search" + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/NewsArticle.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/NewsArticle.swift new file mode 100755 index 0000000..e1f86df --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/NewsArticle.swift @@ -0,0 +1,19 @@ +// +// NewsArticle.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +struct NewsArticle: Decodable, Identifiable { + let title: String + let description: String + let id = UUID() + + private enum CodingKeys: String, CodingKey { + case title = "title" + case description = "description" + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/NewsArticleResponse.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/NewsArticleResponse.swift new file mode 100755 index 0000000..7361efd --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Models/NewsArticleResponse.swift @@ -0,0 +1,12 @@ +// +// NewsArticleResponse.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +struct NewsArticleResponse: Decodable { + let articles: [NewsArticle] +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/MovieAppApp.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/MovieAppApp.swift new file mode 100755 index 0000000..bd073a6 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/MovieAppApp.swift @@ -0,0 +1,17 @@ +// +// MovieAppApp.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import SwiftUI + +@main +struct MovieAppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/MovieDetailView.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/MovieDetailView.swift new file mode 100755 index 0000000..9b4690c --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/MovieDetailView.swift @@ -0,0 +1,47 @@ +// +// MovieDetailView.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import SwiftUI + +struct MovieDetailView: View { + + @StateObject private var store = Store() + let movie: Movie + + var body: some View { + List { + if let movieDetail = store.movieDetail { + AsyncImage(url: movieDetail.poster) + Text(movieDetail.title) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .center) + + if !store.newsArticles.isEmpty { + ForEach(store.newsArticles) { article in + Text(article.title) + } + } else { + Text("No articles found.") + } + + } + }.task { + do { + try await store.fetchMovieById(movie.id) + try await store.fetchArticlesByKeyword(movie.title) + } catch { + print(error) + } + } + } +} + +struct MovieDetailView_Previews: PreviewProvider { + static var previews: some View { + MovieDetailView(movie: Movie.preview) + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Preview Content/Preview Assets.xcassets/Contents.json b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Services/Webservice.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Services/Webservice.swift new file mode 100755 index 0000000..045ecfb --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Services/Webservice.swift @@ -0,0 +1,56 @@ +// +// Webservice.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +enum NetworkError: Error { + case badUrl + case invalidRequest +} + +class Webservice { + + func loadMovies() async throws -> [Movie] { + + guard let url = URL(string: "https://www.omdbapi.com/?s=Batman&page=2&apikey=564727fa") else { + throw NetworkError.badUrl + } + + let (data, _) = try await URLSession.shared.data(from: url) + let movieResponse = try? JSONDecoder().decode(MovieResponse.self, from: data) + return movieResponse?.search ?? [] + } + + func loadMovieBy(_ movieId: String) async throws -> MovieDetail { + + guard let url = URL(string: "http://www.omdbapi.com/?i=\(movieId)&apikey=564727fa") else { + throw NetworkError.badUrl + } + + let (data, _) = try await URLSession.shared.data(from: url) + guard let movieDetail = try? JSONDecoder().decode(MovieDetail.self, from: data) else { + throw NetworkError.invalidRequest + } + + return movieDetail + } + + func loadNewsArticleBy(_ keyword: String) async throws -> [NewsArticle] { + + guard let url = URL(string: "https://newsapi.org/v2/everything?q=\(keyword)&apiKey=0cf790498275413a9247f8b94b3843fd&pageSize=10") else { + throw NetworkError.badUrl + } + + let (data, _) = try await URLSession.shared.data(from: url) + guard let newsArticleResponse = try? JSONDecoder().decode(NewsArticleResponse.self, from: data) else { + throw NetworkError.invalidRequest + } + + return newsArticleResponse.articles + } + +} diff --git a/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Stores/Store.swift b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Stores/Store.swift new file mode 100755 index 0000000..8e7979a --- /dev/null +++ b/SwiftUI/SwiftUIMVPattern_MovieApp/MovieApp/Stores/Store.swift @@ -0,0 +1,28 @@ +// +// Store.swift +// MovieApp +// +// Created by Yilong Chen on 8/8/24. +// + +import Foundation + +@MainActor +class Store: ObservableObject { + + @Published var movies: [Movie] = [] + @Published var newsArticles: [NewsArticle] = [] + @Published var movieDetail: MovieDetail? + + func fetchMovies() async throws { + movies = try await Webservice().loadMovies() + } + + func fetchMovieById(_ movieId: String) async throws { + movieDetail = try await Webservice().loadMovieBy(movieId) + } + + func fetchArticlesByKeyword(_ keyword: String) async throws { + newsArticles = try await Webservice().loadNewsArticleBy(keyword) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp.xcodeproj/project.pbxproj b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp.xcodeproj/project.pbxproj new file mode 100755 index 0000000..d537e25 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp.xcodeproj/project.pbxproj @@ -0,0 +1,482 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 8D0926C125E59B3B00502FF8 /* MovieAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0926C025E59B3B00502FF8 /* MovieAppApp.swift */; }; + 8D0926C525E59B3C00502FF8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D0926C425E59B3C00502FF8 /* Assets.xcassets */; }; + 8D0926C825E59B3C00502FF8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D0926C725E59B3C00502FF8 /* Preview Assets.xcassets */; }; + 8D0926D525E5AE9C00502FF8 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0926D425E5AE9C00502FF8 /* CoreDataManager.swift */; }; + 8D1D27C725FE6CEA0064FA48 /* ActorListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1D27C625FE6CEA0064FA48 /* ActorListScreen.swift */; }; + 8D1D27CA25FE6D3D0064FA48 /* AddActorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1D27C925FE6D3D0064FA48 /* AddActorScreen.swift */; }; + 8D1D27CD25FE75B20064FA48 /* AddActorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1D27CC25FE75B20064FA48 /* AddActorViewModel.swift */; }; + 8D1D27D025FE76160064FA48 /* Actor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1D27CF25FE76160064FA48 /* Actor+Extensions.swift */; }; + 8D45EEC225FFDD38002C48C0 /* ActorListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D45EEC125FFDD38002C48C0 /* ActorListViewModel.swift */; }; + 8D45EEC525FFF869002C48C0 /* ActorDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D45EEC425FFF869002C48C0 /* ActorDetailsScreen.swift */; }; + 8D5C25C325E6EBD0002BAF40 /* AddMovieScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5C25C225E6EBD0002BAF40 /* AddMovieScreen.swift */; }; + 8D5C25C825E6F3D8002BAF40 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5C25C725E6F3D8002BAF40 /* View+Extensions.swift */; }; + 8D5C25D025E6FA7C002BAF40 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5C25CF25E6FA7C002BAF40 /* RatingView.swift */; }; + 8D5C25DF25E72747002BAF40 /* MovieListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5C25DE25E72747002BAF40 /* MovieListScreen.swift */; }; + 8D5C25E225E72CD4002BAF40 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5C25E125E72CD4002BAF40 /* Date+Extensions.swift */; }; + 8D919E1625EE9E5C0029DEFB /* AddReviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D919E1525EE9E5C0029DEFB /* AddReviewScreen.swift */; }; + 8D919E1925EEA0A80029DEFB /* AddReviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D919E1825EEA0A80029DEFB /* AddReviewViewModel.swift */; }; + 8D919E3825EECDB40029DEFB /* ReviewListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D919E3725EECDB40029DEFB /* ReviewListScreen.swift */; }; + 8D919E3B25EED5030029DEFB /* ReviewListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D919E3A25EED5030029DEFB /* ReviewListViewModel.swift */; }; + 8DCCEBF925E8B28900F0B0F3 /* MovieAppModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCEBF725E8B28900F0B0F3 /* MovieAppModel.xcdatamodeld */; }; + 8DCCEBFC25E9765000F0B0F3 /* AddMovieViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCEBFB25E9765000F0B0F3 /* AddMovieViewModel.swift */; }; + 8DCCEBFF25E9C58B00F0B0F3 /* MovieListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCEBFE25E9C58B00F0B0F3 /* MovieListViewModel.swift */; }; + 8DD96A0425F09885002DE8A2 /* Review+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DD96A0325F09885002DE8A2 /* Review+Extensions.swift */; }; + 8DE8803225FA6FB50022B91A /* MovieDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE8803125FA6FB50022B91A /* MovieDetailScreen.swift */; }; + 8DE8803E25FA84720022B91A /* Movie+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE8803D25FA84720022B91A /* Movie+Extensions.swift */; }; + 8DE8804125FA89030022B91A /* BaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE8804025FA89030022B91A /* BaseModel.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8D0926BD25E59B3B00502FF8 /* MovieApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MovieApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D0926C025E59B3B00502FF8 /* MovieAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAppApp.swift; sourceTree = ""; }; + 8D0926C425E59B3C00502FF8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8D0926C725E59B3C00502FF8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8D0926C925E59B3C00502FF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8D0926D425E5AE9C00502FF8 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + 8D1D27C625FE6CEA0064FA48 /* ActorListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorListScreen.swift; sourceTree = ""; }; + 8D1D27C925FE6D3D0064FA48 /* AddActorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddActorScreen.swift; sourceTree = ""; }; + 8D1D27CC25FE75B20064FA48 /* AddActorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddActorViewModel.swift; sourceTree = ""; }; + 8D1D27CF25FE76160064FA48 /* Actor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Extensions.swift"; sourceTree = ""; }; + 8D45EEC125FFDD38002C48C0 /* ActorListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorListViewModel.swift; sourceTree = ""; }; + 8D45EEC425FFF869002C48C0 /* ActorDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorDetailsScreen.swift; sourceTree = ""; }; + 8D5C25C225E6EBD0002BAF40 /* AddMovieScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMovieScreen.swift; sourceTree = ""; }; + 8D5C25C725E6F3D8002BAF40 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; + 8D5C25CF25E6FA7C002BAF40 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = ""; }; + 8D5C25DE25E72747002BAF40 /* MovieListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListScreen.swift; sourceTree = ""; }; + 8D5C25E125E72CD4002BAF40 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; + 8D919E1525EE9E5C0029DEFB /* AddReviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReviewScreen.swift; sourceTree = ""; }; + 8D919E1825EEA0A80029DEFB /* AddReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReviewViewModel.swift; sourceTree = ""; }; + 8D919E3725EECDB40029DEFB /* ReviewListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListScreen.swift; sourceTree = ""; }; + 8D919E3A25EED5030029DEFB /* ReviewListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListViewModel.swift; sourceTree = ""; }; + 8DCCEBF825E8B28900F0B0F3 /* MovieAppModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MovieAppModel.xcdatamodel; sourceTree = ""; }; + 8DCCEBFB25E9765000F0B0F3 /* AddMovieViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMovieViewModel.swift; sourceTree = ""; }; + 8DCCEBFE25E9C58B00F0B0F3 /* MovieListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListViewModel.swift; sourceTree = ""; }; + 8DD96A0325F09885002DE8A2 /* Review+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+Extensions.swift"; sourceTree = ""; }; + 8DE8803125FA6FB50022B91A /* MovieDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailScreen.swift; sourceTree = ""; }; + 8DE8803D25FA84720022B91A /* Movie+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Movie+Extensions.swift"; sourceTree = ""; }; + 8DE8804025FA89030022B91A /* BaseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseModel.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D0926BA25E59B3B00502FF8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8D0926B425E59B3B00502FF8 = { + isa = PBXGroup; + children = ( + 8D0926BF25E59B3B00502FF8 /* MovieApp */, + 8D0926BE25E59B3B00502FF8 /* Products */, + ); + sourceTree = ""; + }; + 8D0926BE25E59B3B00502FF8 /* Products */ = { + isa = PBXGroup; + children = ( + 8D0926BD25E59B3B00502FF8 /* MovieApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 8D0926BF25E59B3B00502FF8 /* MovieApp */ = { + isa = PBXGroup; + children = ( + 8DD96A0225F09872002DE8A2 /* Models */, + 8D5C25CE25E6FA70002BAF40 /* Views */, + 8D5C25C625E6F3CB002BAF40 /* Extensions */, + 8D2C953E25E69E3500459F4F /* Screens */, + 8D2C952C25E6907D00459F4F /* View Models */, + 8D0926D725E5D50400502FF8 /* Managers */, + 8D0926C025E59B3B00502FF8 /* MovieAppApp.swift */, + 8D0926C425E59B3C00502FF8 /* Assets.xcassets */, + 8D0926C925E59B3C00502FF8 /* Info.plist */, + 8D0926C625E59B3C00502FF8 /* Preview Content */, + 8DCCEBF725E8B28900F0B0F3 /* MovieAppModel.xcdatamodeld */, + ); + path = MovieApp; + sourceTree = ""; + }; + 8D0926C625E59B3C00502FF8 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8D0926C725E59B3C00502FF8 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8D0926D725E5D50400502FF8 /* Managers */ = { + isa = PBXGroup; + children = ( + 8D0926D425E5AE9C00502FF8 /* CoreDataManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; + 8D2C952C25E6907D00459F4F /* View Models */ = { + isa = PBXGroup; + children = ( + 8DCCEBFB25E9765000F0B0F3 /* AddMovieViewModel.swift */, + 8DCCEBFE25E9C58B00F0B0F3 /* MovieListViewModel.swift */, + 8D919E1825EEA0A80029DEFB /* AddReviewViewModel.swift */, + 8D919E3A25EED5030029DEFB /* ReviewListViewModel.swift */, + 8D1D27CC25FE75B20064FA48 /* AddActorViewModel.swift */, + 8D45EEC125FFDD38002C48C0 /* ActorListViewModel.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; + 8D2C953E25E69E3500459F4F /* Screens */ = { + isa = PBXGroup; + children = ( + 8D5C25C225E6EBD0002BAF40 /* AddMovieScreen.swift */, + 8D5C25DE25E72747002BAF40 /* MovieListScreen.swift */, + 8D919E1525EE9E5C0029DEFB /* AddReviewScreen.swift */, + 8D919E3725EECDB40029DEFB /* ReviewListScreen.swift */, + 8DE8803125FA6FB50022B91A /* MovieDetailScreen.swift */, + 8D1D27C625FE6CEA0064FA48 /* ActorListScreen.swift */, + 8D1D27C925FE6D3D0064FA48 /* AddActorScreen.swift */, + 8D45EEC425FFF869002C48C0 /* ActorDetailsScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + 8D5C25C625E6F3CB002BAF40 /* Extensions */ = { + isa = PBXGroup; + children = ( + 8D5C25C725E6F3D8002BAF40 /* View+Extensions.swift */, + 8D5C25E125E72CD4002BAF40 /* Date+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 8D5C25CE25E6FA70002BAF40 /* Views */ = { + isa = PBXGroup; + children = ( + 8D5C25CF25E6FA7C002BAF40 /* RatingView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 8DD96A0225F09872002DE8A2 /* Models */ = { + isa = PBXGroup; + children = ( + 8DD96A0325F09885002DE8A2 /* Review+Extensions.swift */, + 8DE8803D25FA84720022B91A /* Movie+Extensions.swift */, + 8DE8804025FA89030022B91A /* BaseModel.swift */, + 8D1D27CF25FE76160064FA48 /* Actor+Extensions.swift */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D0926BC25E59B3B00502FF8 /* MovieApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8D0926CC25E59B3C00502FF8 /* Build configuration list for PBXNativeTarget "MovieApp" */; + buildPhases = ( + 8D0926B925E59B3B00502FF8 /* Sources */, + 8D0926BA25E59B3B00502FF8 /* Frameworks */, + 8D0926BB25E59B3B00502FF8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MovieApp; + productName = MovieApp; + productReference = 8D0926BD25E59B3B00502FF8 /* MovieApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8D0926B525E59B3B00502FF8 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1230; + LastUpgradeCheck = 1230; + TargetAttributes = { + 8D0926BC25E59B3B00502FF8 = { + CreatedOnToolsVersion = 12.3; + }; + }; + }; + buildConfigurationList = 8D0926B825E59B3B00502FF8 /* Build configuration list for PBXProject "MovieApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8D0926B425E59B3B00502FF8; + productRefGroup = 8D0926BE25E59B3B00502FF8 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D0926BC25E59B3B00502FF8 /* MovieApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D0926BB25E59B3B00502FF8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D0926C825E59B3C00502FF8 /* Preview Assets.xcassets in Resources */, + 8D0926C525E59B3C00502FF8 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D0926B925E59B3B00502FF8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D1D27CD25FE75B20064FA48 /* AddActorViewModel.swift in Sources */, + 8D5C25C325E6EBD0002BAF40 /* AddMovieScreen.swift in Sources */, + 8D1D27CA25FE6D3D0064FA48 /* AddActorScreen.swift in Sources */, + 8DCCEBFC25E9765000F0B0F3 /* AddMovieViewModel.swift in Sources */, + 8D919E3B25EED5030029DEFB /* ReviewListViewModel.swift in Sources */, + 8D5C25E225E72CD4002BAF40 /* Date+Extensions.swift in Sources */, + 8D0926D525E5AE9C00502FF8 /* CoreDataManager.swift in Sources */, + 8DE8803225FA6FB50022B91A /* MovieDetailScreen.swift in Sources */, + 8D5C25C825E6F3D8002BAF40 /* View+Extensions.swift in Sources */, + 8DE8803E25FA84720022B91A /* Movie+Extensions.swift in Sources */, + 8D45EEC525FFF869002C48C0 /* ActorDetailsScreen.swift in Sources */, + 8DCCEBF925E8B28900F0B0F3 /* MovieAppModel.xcdatamodeld in Sources */, + 8DD96A0425F09885002DE8A2 /* Review+Extensions.swift in Sources */, + 8D5C25DF25E72747002BAF40 /* MovieListScreen.swift in Sources */, + 8D919E1625EE9E5C0029DEFB /* AddReviewScreen.swift in Sources */, + 8D45EEC225FFDD38002C48C0 /* ActorListViewModel.swift in Sources */, + 8D0926C125E59B3B00502FF8 /* MovieAppApp.swift in Sources */, + 8D5C25D025E6FA7C002BAF40 /* RatingView.swift in Sources */, + 8D919E1925EEA0A80029DEFB /* AddReviewViewModel.swift in Sources */, + 8D1D27D025FE76160064FA48 /* Actor+Extensions.swift in Sources */, + 8DE8804125FA89030022B91A /* BaseModel.swift in Sources */, + 8D919E3825EECDB40029DEFB /* ReviewListScreen.swift in Sources */, + 8DCCEBFF25E9C58B00F0B0F3 /* MovieListViewModel.swift in Sources */, + 8D1D27C725FE6CEA0064FA48 /* ActorListScreen.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8D0926CA25E59B3C00502FF8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8D0926CB25E59B3C00502FF8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8D0926CD25E59B3C00502FF8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"MovieApp/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = MovieApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.MovieApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8D0926CE25E59B3C00502FF8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"MovieApp/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = MovieApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.MovieApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8D0926B825E59B3B00502FF8 /* Build configuration list for PBXProject "MovieApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8D0926CA25E59B3C00502FF8 /* Debug */, + 8D0926CB25E59B3C00502FF8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8D0926CC25E59B3C00502FF8 /* Build configuration list for PBXNativeTarget "MovieApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8D0926CD25E59B3C00502FF8 /* Debug */, + 8D0926CE25E59B3C00502FF8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 8DCCEBF725E8B28900F0B0F3 /* MovieAppModel.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 8DCCEBF825E8B28900F0B0F3 /* MovieAppModel.xcdatamodel */, + ); + currentVersion = 8DCCEBF825E8B28900F0B0F3 /* MovieAppModel.xcdatamodel */; + path = MovieAppModel.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 8D0926B525E59B3B00502FF8 /* Project object */; +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp.xcodeproj/xcshareddata/xcschemes/MovieApp.xcscheme b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp.xcodeproj/xcshareddata/xcschemes/MovieApp.xcscheme new file mode 100755 index 0000000..de03cd0 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp.xcodeproj/xcshareddata/xcschemes/MovieApp.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100755 index 0000000..eb87897 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..9221b9b --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/Contents.json b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Extensions/Date+Extensions.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Extensions/Date+Extensions.swift new file mode 100755 index 0000000..8750ad7 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Extensions/Date+Extensions.swift @@ -0,0 +1,18 @@ +// +// Date+Extensions.swift +// MovieApp +// +// Created by Yilong Chen on 2/24/24. +// + +import Foundation + +extension Date { + + func asFormattedString() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "MM/dd/yyyy" + return formatter.string(from: self) + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Extensions/View+Extensions.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Extensions/View+Extensions.swift new file mode 100755 index 0000000..5e0e76b --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Extensions/View+Extensions.swift @@ -0,0 +1,15 @@ +// +// View+Extensions.swift +// MovieApp +// +// Created by Yilong Chen on 2/24/24. +// + +import Foundation +import SwiftUI + +extension View { + func embedInNavigationView() -> some View { + NavigationView { self } + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Info.plist b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Info.plist new file mode 100755 index 0000000..efc211a --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Managers/CoreDataManager.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Managers/CoreDataManager.swift new file mode 100755 index 0000000..e19866e --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Managers/CoreDataManager.swift @@ -0,0 +1,37 @@ +// +// CoreDataManager.swift +// MovieApp +// +// Created by Yilong Chen on 2/23/24. +// + +import Foundation +import CoreData +import SwiftUI + +class CoreDataManager { + + let persistentContainer: NSPersistentContainer + + static let shared = CoreDataManager() + + private init() { + + persistentContainer = NSPersistentContainer(name: "MovieAppModel") + persistentContainer.loadPersistentStores { (description, error) in + if let error = error { + fatalError("Failed to initialize Core Data \(error)") + } + } + + let directories = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + print(directories[0]) + } + + var viewContext: NSManagedObjectContext { + return persistentContainer.viewContext + } + +} + + diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Actor+Extensions.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Actor+Extensions.swift new file mode 100755 index 0000000..08e5ff3 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Actor+Extensions.swift @@ -0,0 +1,23 @@ +// +// Actor+Extensions.swift +// MovieApp +// +// Created by Yilong Chen on 3/14/24. +// + +import Foundation +import CoreData + +extension Actor: BaseModel { + + static func getActorsByMovieId(movieId: NSManagedObjectID) -> [Actor] { + guard let movie = Movie.byId(id: movieId) as? Movie, + let actors = movie.actors + else { + return [] + } + + return (actors.allObjects as? [Actor]) ?? [] + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/BaseModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/BaseModel.swift new file mode 100755 index 0000000..cd67c2b --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/BaseModel.swift @@ -0,0 +1,57 @@ +// +// BaseModel.swift +// MovieApp +// +// Created by Yilong Chen on 3/11/24. +// + +import Foundation +import CoreData + +protocol BaseModel: NSManagedObject { + func save() throws + func delete() throws + static func byId(id: NSManagedObjectID) -> T? + static func all() -> [T] +} + +extension BaseModel { + + static var viewContext: NSManagedObjectContext { + return CoreDataManager.shared.viewContext + } + + func save() throws { + do { + try Self.viewContext.save() + } catch { + throw error + } + } + + func delete() throws { + Self.viewContext.delete(self) + try save() + } + + static func all() -> [T] where T: NSManagedObject { + + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: String(describing: T.self)) + + do { + return try viewContext.fetch(fetchRequest) + } catch { + return [] + } + } + + static func byId(id: NSManagedObjectID) -> T? where T: NSManagedObject { + do { + return try viewContext.existingObject(with: id) as? T + } catch { + print(error) + return nil + } + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Movie+Extensions.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Movie+Extensions.swift new file mode 100755 index 0000000..611c586 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Movie+Extensions.swift @@ -0,0 +1,27 @@ +// +// Movie+Extensions.swift +// MovieApp +// +// Created by Yilong Chen on 3/11/24. +// + +import Foundation +import CoreData + +extension Movie: BaseModel { + + static func byActorName(name: String) -> [Movie] { + + let request: NSFetchRequest = Movie.fetchRequest() + request.predicate = NSPredicate(format: "actors.name CONTAINS %@", name) + + do { + return try viewContext.fetch(request) + } catch { + print(error) + return [] + } + + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Review+Extensions.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Review+Extensions.swift new file mode 100755 index 0000000..3ef866b --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Models/Review+Extensions.swift @@ -0,0 +1,26 @@ +// +// Review+Extensions.swift +// MovieApp +// +// Created by Yilong Chen on 3/3/24. +// + +import Foundation +import CoreData + +extension Review: BaseModel { + + static func getReviewsByMovieId(movieId: NSManagedObjectID) -> [Review] { + + let request: NSFetchRequest = Review.fetchRequest() + request.predicate = NSPredicate(format: "movie = %@", movieId) + + do { + return try viewContext.fetch(request) + } catch { + return [] + } + + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/MovieAppApp.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/MovieAppApp.swift new file mode 100755 index 0000000..a480af7 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/MovieAppApp.swift @@ -0,0 +1,22 @@ +// +// MovieAppApp.swift +// MovieApp +// +// Created by Yilong Chen on 2/23/24. +// + +import SwiftUI + +@main +struct MovieAppApp: App { + + init() { + UITableView.appearance().separatorStyle = .none + } + + var body: some Scene { + WindowGroup { + MovieListScreen() + } + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/MovieAppModel.xcdatamodeld/MovieAppModel.xcdatamodel/contents b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/MovieAppModel.xcdatamodeld/MovieAppModel.xcdatamodel/contents new file mode 100755 index 0000000..e4683a8 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/MovieAppModel.xcdatamodeld/MovieAppModel.xcdatamodel/contents @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Preview Content/Preview Assets.xcassets/Contents.json b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ActorDetailsScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ActorDetailsScreen.swift new file mode 100755 index 0000000..78b9332 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ActorDetailsScreen.swift @@ -0,0 +1,30 @@ +// +// ActorDetailsScreen.swift +// MovieApp +// +// Created by Yilong Chen on 3/15/24. +// + +import SwiftUI + +struct ActorDetailsScreen: View { + + let actor: ActorViewModel + + var body: some View { + VStack { + List(actor.movies, id: \.movieId) { movie in + MovieCell(movie: movie) + }.listStyle(PlainListStyle()) + + }.navigationTitle(actor.name) + } +} + +struct ActorDetailsScreen_Previews: PreviewProvider { + static var previews: some View { + + let actorVM = ActorViewModel(actor: Actor(context: Actor.viewContext)) + ActorDetailsScreen(actor: actorVM) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ActorListScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ActorListScreen.swift new file mode 100755 index 0000000..6af5c06 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ActorListScreen.swift @@ -0,0 +1,71 @@ +// +// ActorListScreen.swift +// MovieApp +// +// Created by Yilong Chen on 3/11/24. +// + +import SwiftUI + +struct ActorListScreen: View { + + @State private var isPresented: Bool = false + @StateObject private var actorListVM = ActorListViewModel() + let movie: MovieViewModel + + var body: some View { + + List { + + Section(header: Text("Actors")) { + ForEach(actorListVM.actors, id: \.actorId) { actor in + + HStack { + NavigationLink( + destination: ActorDetailsScreen(actor: actor), + label: { + Text(actor.name) + .foregroundColor(.black) + }) + Spacer() + } + .padding(10) + .background(LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 0.9567790627, green: 0.9569163918, blue: 0.9567491412, alpha: 1)), Color(#colorLiteral(red: 0.9685427547, green: 0.9686816335, blue: 0.9685124755, alpha: 1))]), startPoint: .leading, endPoint: /*@START_MENU_TOKEN@*/.trailing/*@END_MENU_TOKEN@*/)) + .clipShape(RoundedRectangle(cornerRadius: 10, style: /*@START_MENU_TOKEN@*/.continuous/*@END_MENU_TOKEN@*/)) + } + } + + }.listStyle(PlainListStyle()) + + + .onAppear(perform: { + actorListVM.getActorsByMovie(vm: movie) + }) + .sheet(isPresented: $isPresented, onDismiss: { + actorListVM.getActorsByMovie(vm: movie) + }, content: { + AddActorScreen(movie: movie) + }) + .navigationTitle(movie.title) + .navigationBarItems(trailing: Button(action: { + isPresented = true + }, label: { + Image(systemName: "plus") + })) + + } +} + +struct ActorListScreen_Previews: PreviewProvider { + static var previews: some View { + + let movie = Movie(context: CoreDataManager.shared.viewContext) + movie.title = "Lord of the Rings" + let actor = Actor(context: CoreDataManager.shared.viewContext) + actor.name = "Tom Hanks" + movie.addToActors(actor) + + return ActorListScreen(movie: MovieViewModel(movie: movie)) + .embedInNavigationView() + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddActorScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddActorScreen.swift new file mode 100755 index 0000000..e360207 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddActorScreen.swift @@ -0,0 +1,45 @@ +// +// AddActorScreen.swift +// MovieApp +// +// Created by Yilong Chen on 3/11/24. +// + +import SwiftUI + +struct AddActorScreen: View { + + let movie: MovieViewModel + @StateObject private var addActorVM = AddActorViewModel() + @Environment(\.presentationMode) private var presentationMode + + var body: some View { + + Form { + VStack(alignment: .leading) { + Text("Add Actor") + .font(.largeTitle) + Text(movie.title) + }.padding(.bottom, 50) + TextField("Enter name", text: $addActorVM.name) + HStack { + Spacer() + Button("Cancel") { + presentationMode.wrappedValue.dismiss() + }.buttonStyle(PlainButtonStyle()) + Spacer() + Button("Save") { + addActorVM.addActorToMovie(movieId: movie.movieId) + presentationMode.wrappedValue.dismiss() + }.buttonStyle(PlainButtonStyle()) + Spacer() + } + } + } +} + +struct AddActorScreen_Previews: PreviewProvider { + static var previews: some View { + AddActorScreen(movie: MovieViewModel(movie: Movie(context: Movie.viewContext))) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddMovieScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddMovieScreen.swift new file mode 100755 index 0000000..778ecac --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddMovieScreen.swift @@ -0,0 +1,45 @@ +// +// AddMovieScreen.swift +// MovieApp +// +// Created by Yilong Chen on 2/24/24. +// + +import SwiftUI + +struct AddMovieScreen: View { + + @StateObject private var addMovieVM = AddMovieViewModel() + @Environment(\.presentationMode) var presentationMode + + var body: some View { + Form { + TextField("Enter name", text: $addMovieVM.title) + TextField("Enter director", text: $addMovieVM.director) + HStack { + Text("Rating") + Spacer() + RatingView(rating: $addMovieVM.rating) + } + DatePicker("Release Date", selection: $addMovieVM.releaseDate) + + HStack { + Spacer() + Button("Save") { + addMovieVM.save() + presentationMode.wrappedValue.dismiss() + } + Spacer() + } + + } + .navigationTitle("Add Movie") + .embedInNavigationView() + } +} + +struct AddMovieScreen_Previews: PreviewProvider { + static var previews: some View { + AddMovieScreen() + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddReviewScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddReviewScreen.swift new file mode 100755 index 0000000..626b934 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/AddReviewScreen.swift @@ -0,0 +1,43 @@ +// +// AddReviewScreen.swift +// MovieApp +// +// Created by Yilong Chen on 3/2/24. +// + +import SwiftUI + +struct AddReviewScreen: View { + + @StateObject private var addReviewVM = AddReviewViewModel() + @Environment(\.presentationMode) var presentationMode + + let movie: MovieViewModel + + var body: some View { + Form { + TextField("Enter title", text: $addReviewVM.title) + TextEditor(text: $addReviewVM.text) + + HStack { + Spacer() + Button("Save") { + addReviewVM.addReviewForMovie(vm: movie) + presentationMode.wrappedValue.dismiss() + } + Spacer() + } + + } + .navigationTitle("Add Review") + .embedInNavigationView() + } +} + +struct AddReviewScreen_Previews: PreviewProvider { + static var previews: some View { + + let movie = MovieViewModel(movie: Movie(context: CoreDataManager.shared.viewContext)) + AddReviewScreen(movie: movie) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/MovieDetailScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/MovieDetailScreen.swift new file mode 100755 index 0000000..3e50681 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/MovieDetailScreen.swift @@ -0,0 +1,62 @@ +// +// MovieDetailScreen.swift +// MovieApp +// +// Created by Yilong Chen on 3/11/24. +// + +import SwiftUI + +enum MovieDetailsRoutes: Identifiable, CaseIterable { + + var id: UUID { + return UUID() + } + + case reviews + case actors +} + +extension MovieDetailsRoutes { + + var displayText: String { + switch self { + case .reviews: + return "Reviews" + case .actors: + return "Actors" + } + } +} + +struct MovieDetailScreen: View { + + let movie: MovieViewModel + + var body: some View { + VStack { + List { + NavigationLink( + destination: ReviewListScreen(movie: movie), + label: { + Text("Reviews") + }) + + NavigationLink( + destination: ActorListScreen(movie: movie), + label: { + Text("Actors") + }) + + }.listStyle(PlainListStyle()) + }.navigationTitle(movie.title) + } +} + +struct MovieDetailScreen_Previews: PreviewProvider { + static var previews: some View { + + MovieDetailScreen(movie: MovieViewModel(movie: Movie(context: CoreDataManager.shared.viewContext))) + .embedInNavigationView() + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/MovieListScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/MovieListScreen.swift new file mode 100755 index 0000000..4b73998 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/MovieListScreen.swift @@ -0,0 +1,93 @@ +// +// MovieListScreen.swift +// MovieApp +// +// Created by Yilong Chen on 2/24/24. +// + +import SwiftUI + +struct MovieListScreen: View { + + @StateObject private var movieListVM = MovieListViewModel() + @State private var isPresented: Bool = false + + private func deleteMovie(at indexSet: IndexSet) { + indexSet.forEach { index in + let movie = movieListVM.movies[index] + // delete the movie + movieListVM.deleteMovie(movie: movie) + // get all movies + movieListVM.getAllMovies() + } + } + + var body: some View { + List { + + ForEach(movieListVM.movies, id: \.movieId) { movie in + NavigationLink( + destination: MovieDetailScreen(movie: movie), + label: { + MovieCell(movie: movie) + }) + }.onDelete(perform: deleteMovie) + + }.listStyle(PlainListStyle()) + + .navigationTitle("Movies") + .navigationBarItems(trailing: Button("Add Movie") { + isPresented = true + }) + .sheet(isPresented: $isPresented, onDismiss: { + movieListVM.getAllMovies() + }, content: { + AddMovieScreen() + }) + .embedInNavigationView() + + .onAppear(perform: { + UITableView.appearance().separatorStyle = .none + UITableView.appearance().separatorColor = .clear + movieListVM.getAllMovies() + }) + } +} + +struct MovieListScreen_Previews: PreviewProvider { + static var previews: some View { + Group { + MovieListScreen() + } + } +} + +struct MovieCell: View { + + let movie: MovieViewModel + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 5) { + Text(movie.title) + .fontWeight(.bold) + .font(.system(size: 22)) + Text(movie.director) + .font(.callout) + .opacity(0.5) + Spacer() + + } + Spacer() + HStack { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + Text("\(movie.rating!)") + } + } + .padding() + .foregroundColor(Color.black) + .background(LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 0.9567790627, green: 0.9569163918, blue: 0.9567491412, alpha: 1)), Color(#colorLiteral(red: 0.9685427547, green: 0.9686816335, blue: 0.9685124755, alpha: 1))]), startPoint: .leading, endPoint: /*@START_MENU_TOKEN@*/.trailing/*@END_MENU_TOKEN@*/)) + .clipShape(RoundedRectangle(cornerRadius: 15.0, style: .continuous)) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ReviewListScreen.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ReviewListScreen.swift new file mode 100755 index 0000000..2b494ee --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Screens/ReviewListScreen.swift @@ -0,0 +1,56 @@ +// +// ReviewListScreen.swift +// MovieApp +// +// Created by Yilong Chen on 3/2/24. +// + +import SwiftUI + +struct ReviewListScreen: View { + + let movie: MovieViewModel + @State private var isPresented: Bool = false + @StateObject private var reviewListVM = ReviewListViewModel() + + var body: some View { + VStack { + List { + Section(header: Text("Reviews")) { + ForEach(reviewListVM.reviews, id: \.reviewId) { review in + HStack { + VStack(alignment: .leading) { + Text(review.title) + Text(review.text) + .font(.caption) + } + Spacer() + Text(review.publishedDate!.asFormattedString()) + } + } + } + } + } + .navigationTitle(movie.title) + .navigationBarItems(trailing: Button("Add New Review") { + isPresented = true + }) + .sheet(isPresented: $isPresented, onDismiss: { + reviewListVM.getReviewsByMovie(vm: movie) + }, content: { + AddReviewScreen(movie: movie) + }) + .onAppear(perform: { + reviewListVM.getReviewsByMovie(vm: movie) + }) + } +} + +struct ReviewListScreen_Previews: PreviewProvider { + static var previews: some View { + + let movie = MovieViewModel(movie: Movie(context: CoreDataManager.shared.viewContext)) + ReviewListScreen(movie: movie).embedInNavigationView() + } +} + diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/ActorListViewModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/ActorListViewModel.swift new file mode 100755 index 0000000..c6ba5d7 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/ActorListViewModel.swift @@ -0,0 +1,38 @@ +// +// ActorListViewModel.swift +// MovieApp +// +// Created by Yilong Chen on 3/15/24. +// + +import Foundation +import CoreData + +class ActorListViewModel: ObservableObject { + + @Published var actors = [ActorViewModel]() + + func getActorsByMovie(vm: MovieViewModel) { + DispatchQueue.main.async { + self.actors = Actor.getActorsByMovieId(movieId: vm.movieId).map(ActorViewModel.init) + } + } + +} + +struct ActorViewModel { + + let actor: Actor + + var actorId: NSManagedObjectID { + return actor.objectID + } + + var name: String { + return actor.name ?? "" + } + + var movies: [MovieViewModel] { + return Movie.byActorName(name: name).map(MovieViewModel.init) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddActorViewModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddActorViewModel.swift new file mode 100755 index 0000000..bae999f --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddActorViewModel.swift @@ -0,0 +1,29 @@ +// +// AddActorViewModel.swift +// MovieApp +// +// Created by Yilong Chen on 3/14/24. +// + +import Foundation +import CoreData + +class AddActorViewModel: ObservableObject { + + var name: String = "" + + func addActorToMovie(movieId: NSManagedObjectID) { + + let movie: Movie? = Movie.byId(id: movieId) + + if let movie = movie { + let actor = Actor(context: Actor.viewContext) + actor.name = name + actor.addToMovies(movie) + + try? actor.save() + } + + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddMovieViewModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddMovieViewModel.swift new file mode 100755 index 0000000..40ec673 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddMovieViewModel.swift @@ -0,0 +1,28 @@ +// +// AddMovieViewModel.swift +// MovieApp +// +// Created by Yilong Chen on 2/26/24. +// + +import Foundation + +class AddMovieViewModel: ObservableObject { + + var title: String = "" + var director: String = "" + @Published var rating: Int? = nil + var releaseDate: Date = Date() + + func save() { + + let movie = Movie(context: Movie.viewContext) + movie.title = title + movie.director = director + movie.rating = Double(rating ?? 0) + movie.releaseDate = releaseDate + + try? movie.save() + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddReviewViewModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddReviewViewModel.swift new file mode 100755 index 0000000..ff2e410 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/AddReviewViewModel.swift @@ -0,0 +1,28 @@ +// +// AddReviewViewModel.swift +// MovieApp +// +// Created by Yilong Chen on 3/2/24. +// + +import Foundation + +class AddReviewViewModel: ObservableObject { + + var title: String = "" + var text: String = "" + + func addReviewForMovie(vm: MovieViewModel) { + + let movie: Movie? = Movie.byId(id: vm.movieId) + + let review = Review(context: Movie.viewContext) + review.title = title + review.text = text + review.movie = movie + + // save the review + try? review.save() + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/MovieListViewModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/MovieListViewModel.swift new file mode 100755 index 0000000..3949dd8 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/MovieListViewModel.swift @@ -0,0 +1,52 @@ +// +// MovieListViewModel.swift +// MovieApp +// +// Created by Yilong Chen on 2/26/24. +// + +import Foundation +import CoreData + +class MovieListViewModel: ObservableObject { + + @Published var movies = [MovieViewModel]() + + func deleteMovie(movie: MovieViewModel) { + let movie: Movie? = Movie.byId(id: movie.movieId) + if let movie = movie { + try? movie.delete() + } + } + + func getAllMovies() { + DispatchQueue.main.async { + self.movies = Movie.all().map(MovieViewModel.init) + } + } +} + +struct MovieViewModel { + + let movie: Movie + + var movieId: NSManagedObjectID { + return movie.objectID + } + + var title: String { + return movie.title ?? "" + } + + var director: String { + return movie.director ?? "Not available" + } + + var releaseDate: String? { + return movie.releaseDate?.asFormattedString() + } + + var rating: Int? { + return Int(movie.rating) + } +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/ReviewListViewModel.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/ReviewListViewModel.swift new file mode 100755 index 0000000..ef00620 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/View Models/ReviewListViewModel.swift @@ -0,0 +1,42 @@ +// +// ReviewListViewModel.swift +// MovieApp +// +// Created by Yilong Chen on 3/2/24. +// + +import Foundation +import CoreData + +class ReviewListViewModel: ObservableObject { + + @Published var reviews = [ReviewViewModel]() + + func getReviewsByMovie(vm: MovieViewModel) { + DispatchQueue.main.async { + self.reviews = Review.getReviewsByMovieId(movieId: vm.movieId).map(ReviewViewModel.init) + } + } +} + +struct ReviewViewModel { + + let review: Review + + var reviewId: NSManagedObjectID { + return review.objectID + } + + var title: String { + return review.title ?? "" + } + + var text: String { + return review.text ?? "" + } + + var publishedDate: Date? { + return review.publishedAt + } + +} diff --git a/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Views/RatingView.swift b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Views/RatingView.swift new file mode 100755 index 0000000..ae291e4 --- /dev/null +++ b/SwiftUI/SwiftUIMVVM_MovieApp/MovieApp/Views/RatingView.swift @@ -0,0 +1,42 @@ +// +// RatingView.swift +// SwiftUIRatings +// +// Created by Yilong Chen on 6/20/20. +// Copyright © 2020 Yilong Chen. All rights reserved. +// + +import SwiftUI + +struct RatingView: View { + + @Binding var rating: Int? + + private func starType(index: Int) -> String { + + if let rating = self.rating { + return index <= rating ? "star.fill" : "star" + } else { + return "star" + } + + } + + var body: some View { + HStack { + ForEach(1...5, id: \.self) { index in + Image(systemName: self.starType(index: index)) + .foregroundColor(Color.orange) + .onTapGesture { + self.rating = index + } + } + } + } +} + +struct RatingView_Previews: PreviewProvider { + static var previews: some View { + RatingView(rating: .constant(3)) + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux.xcodeproj/project.pbxproj b/SwiftUI/SwiftUIRedux/SwiftUIRedux.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ef5e5c5 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux.xcodeproj/project.pbxproj @@ -0,0 +1,652 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 9A970F312C435CEB0043EC5E /* SwiftUIReduxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A970F302C435CEB0043EC5E /* SwiftUIReduxApp.swift */; }; + 9A970F332C435CEB0043EC5E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A970F322C435CEB0043EC5E /* ContentView.swift */; }; + 9A970F352C435CED0043EC5E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A970F342C435CED0043EC5E /* Assets.xcassets */; }; + 9A970F382C435CED0043EC5E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A970F372C435CED0043EC5E /* Preview Assets.xcassets */; }; + 9A970F422C435CED0043EC5E /* SwiftUIReduxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A970F412C435CED0043EC5E /* SwiftUIReduxTests.swift */; }; + 9A970F4C2C435CED0043EC5E /* SwiftUIReduxUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A970F4B2C435CED0043EC5E /* SwiftUIReduxUITests.swift */; }; + 9A970F4E2C435CED0043EC5E /* SwiftUIReduxUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A970F4D2C435CED0043EC5E /* SwiftUIReduxUITestsLaunchTests.swift */; }; + 9A970F5C2C435D170043EC5E /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A970F5B2C435D170043EC5E /* Store.swift */; }; + 9ADC410B2C43862600D8500A /* CounterReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC410A2C43862600D8500A /* CounterReducer.swift */; }; + 9ADC410F2C4386A400D8500A /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC410E2C4386A400D8500A /* AppReducer.swift */; }; + 9ADC41122C43C27000D8500A /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC41112C43C27000D8500A /* Task.swift */; }; + 9ADC41142C43C30B00D8500A /* TaskReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC41132C43C30B00D8500A /* TaskReducer.swift */; }; + 9ADC41162C43C9F100D8500A /* AddTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC41152C43C9F100D8500A /* AddTaskView.swift */; }; + 9ADC41192C43D59800D8500A /* LogMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC41182C43D59800D8500A /* LogMiddleware.swift */; }; + 9ADC411B2C43D88E00D8500A /* IncrementMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC411A2C43D88E00D8500A /* IncrementMiddleware.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 9A970F3E2C435CED0043EC5E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A970F252C435CEB0043EC5E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A970F2C2C435CEB0043EC5E; + remoteInfo = SwiftUIRedux; + }; + 9A970F482C435CED0043EC5E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A970F252C435CEB0043EC5E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A970F2C2C435CEB0043EC5E; + remoteInfo = SwiftUIRedux; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 9A970F2D2C435CEB0043EC5E /* SwiftUIRedux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIRedux.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9A970F302C435CEB0043EC5E /* SwiftUIReduxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIReduxApp.swift; sourceTree = ""; }; + 9A970F322C435CEB0043EC5E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 9A970F342C435CED0043EC5E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9A970F372C435CED0043EC5E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 9A970F3D2C435CED0043EC5E /* SwiftUIReduxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftUIReduxTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9A970F412C435CED0043EC5E /* SwiftUIReduxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIReduxTests.swift; sourceTree = ""; }; + 9A970F472C435CED0043EC5E /* SwiftUIReduxUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftUIReduxUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9A970F4B2C435CED0043EC5E /* SwiftUIReduxUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIReduxUITests.swift; sourceTree = ""; }; + 9A970F4D2C435CED0043EC5E /* SwiftUIReduxUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIReduxUITestsLaunchTests.swift; sourceTree = ""; }; + 9A970F5B2C435D170043EC5E /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 9ADC410A2C43862600D8500A /* CounterReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterReducer.swift; sourceTree = ""; }; + 9ADC410E2C4386A400D8500A /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; + 9ADC41112C43C27000D8500A /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; + 9ADC41132C43C30B00D8500A /* TaskReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskReducer.swift; sourceTree = ""; }; + 9ADC41152C43C9F100D8500A /* AddTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTaskView.swift; sourceTree = ""; }; + 9ADC41182C43D59800D8500A /* LogMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogMiddleware.swift; sourceTree = ""; }; + 9ADC411A2C43D88E00D8500A /* IncrementMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncrementMiddleware.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9A970F2A2C435CEB0043EC5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A970F3A2C435CED0043EC5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A970F442C435CED0043EC5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9A970F242C435CEB0043EC5E = { + isa = PBXGroup; + children = ( + 9A970F2F2C435CEB0043EC5E /* SwiftUIRedux */, + 9A970F402C435CED0043EC5E /* SwiftUIReduxTests */, + 9A970F4A2C435CED0043EC5E /* SwiftUIReduxUITests */, + 9A970F2E2C435CEB0043EC5E /* Products */, + ); + sourceTree = ""; + }; + 9A970F2E2C435CEB0043EC5E /* Products */ = { + isa = PBXGroup; + children = ( + 9A970F2D2C435CEB0043EC5E /* SwiftUIRedux.app */, + 9A970F3D2C435CED0043EC5E /* SwiftUIReduxTests.xctest */, + 9A970F472C435CED0043EC5E /* SwiftUIReduxUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 9A970F2F2C435CEB0043EC5E /* SwiftUIRedux */ = { + isa = PBXGroup; + children = ( + 9ADC41102C43C25B00D8500A /* Models */, + 9A970F5A2C435D0B0043EC5E /* Store */, + 9A970F302C435CEB0043EC5E /* SwiftUIReduxApp.swift */, + 9A970F322C435CEB0043EC5E /* ContentView.swift */, + 9ADC41152C43C9F100D8500A /* AddTaskView.swift */, + 9A970F342C435CED0043EC5E /* Assets.xcassets */, + 9A970F362C435CED0043EC5E /* Preview Content */, + ); + path = SwiftUIRedux; + sourceTree = ""; + }; + 9A970F362C435CED0043EC5E /* Preview Content */ = { + isa = PBXGroup; + children = ( + 9A970F372C435CED0043EC5E /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 9A970F402C435CED0043EC5E /* SwiftUIReduxTests */ = { + isa = PBXGroup; + children = ( + 9A970F412C435CED0043EC5E /* SwiftUIReduxTests.swift */, + ); + path = SwiftUIReduxTests; + sourceTree = ""; + }; + 9A970F4A2C435CED0043EC5E /* SwiftUIReduxUITests */ = { + isa = PBXGroup; + children = ( + 9A970F4B2C435CED0043EC5E /* SwiftUIReduxUITests.swift */, + 9A970F4D2C435CED0043EC5E /* SwiftUIReduxUITestsLaunchTests.swift */, + ); + path = SwiftUIReduxUITests; + sourceTree = ""; + }; + 9A970F5A2C435D0B0043EC5E /* Store */ = { + isa = PBXGroup; + children = ( + 9ADC41172C43D57D00D8500A /* middlewares */, + 9ADC41092C43860400D8500A /* reducers */, + 9A970F5B2C435D170043EC5E /* Store.swift */, + ); + path = Store; + sourceTree = ""; + }; + 9ADC41092C43860400D8500A /* reducers */ = { + isa = PBXGroup; + children = ( + 9ADC410A2C43862600D8500A /* CounterReducer.swift */, + 9ADC410E2C4386A400D8500A /* AppReducer.swift */, + 9ADC41132C43C30B00D8500A /* TaskReducer.swift */, + ); + path = reducers; + sourceTree = ""; + }; + 9ADC41102C43C25B00D8500A /* Models */ = { + isa = PBXGroup; + children = ( + 9ADC41112C43C27000D8500A /* Task.swift */, + ); + path = Models; + sourceTree = ""; + }; + 9ADC41172C43D57D00D8500A /* middlewares */ = { + isa = PBXGroup; + children = ( + 9ADC41182C43D59800D8500A /* LogMiddleware.swift */, + 9ADC411A2C43D88E00D8500A /* IncrementMiddleware.swift */, + ); + path = middlewares; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 9A970F2C2C435CEB0043EC5E /* SwiftUIRedux */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9A970F512C435CED0043EC5E /* Build configuration list for PBXNativeTarget "SwiftUIRedux" */; + buildPhases = ( + 9A970F292C435CEB0043EC5E /* Sources */, + 9A970F2A2C435CEB0043EC5E /* Frameworks */, + 9A970F2B2C435CEB0043EC5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftUIRedux; + productName = SwiftUIRedux; + productReference = 9A970F2D2C435CEB0043EC5E /* SwiftUIRedux.app */; + productType = "com.apple.product-type.application"; + }; + 9A970F3C2C435CED0043EC5E /* SwiftUIReduxTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9A970F542C435CED0043EC5E /* Build configuration list for PBXNativeTarget "SwiftUIReduxTests" */; + buildPhases = ( + 9A970F392C435CED0043EC5E /* Sources */, + 9A970F3A2C435CED0043EC5E /* Frameworks */, + 9A970F3B2C435CED0043EC5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9A970F3F2C435CED0043EC5E /* PBXTargetDependency */, + ); + name = SwiftUIReduxTests; + productName = SwiftUIReduxTests; + productReference = 9A970F3D2C435CED0043EC5E /* SwiftUIReduxTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 9A970F462C435CED0043EC5E /* SwiftUIReduxUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9A970F572C435CED0043EC5E /* Build configuration list for PBXNativeTarget "SwiftUIReduxUITests" */; + buildPhases = ( + 9A970F432C435CED0043EC5E /* Sources */, + 9A970F442C435CED0043EC5E /* Frameworks */, + 9A970F452C435CED0043EC5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9A970F492C435CED0043EC5E /* PBXTargetDependency */, + ); + name = SwiftUIReduxUITests; + productName = SwiftUIReduxUITests; + productReference = 9A970F472C435CED0043EC5E /* SwiftUIReduxUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9A970F252C435CEB0043EC5E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + 9A970F2C2C435CEB0043EC5E = { + CreatedOnToolsVersion = 14.2; + }; + 9A970F3C2C435CED0043EC5E = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 9A970F2C2C435CEB0043EC5E; + }; + 9A970F462C435CED0043EC5E = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 9A970F2C2C435CEB0043EC5E; + }; + }; + }; + buildConfigurationList = 9A970F282C435CEB0043EC5E /* Build configuration list for PBXProject "SwiftUIRedux" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9A970F242C435CEB0043EC5E; + productRefGroup = 9A970F2E2C435CEB0043EC5E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 9A970F2C2C435CEB0043EC5E /* SwiftUIRedux */, + 9A970F3C2C435CED0043EC5E /* SwiftUIReduxTests */, + 9A970F462C435CED0043EC5E /* SwiftUIReduxUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9A970F2B2C435CEB0043EC5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A970F382C435CED0043EC5E /* Preview Assets.xcassets in Resources */, + 9A970F352C435CED0043EC5E /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A970F3B2C435CED0043EC5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A970F452C435CED0043EC5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 9A970F292C435CEB0043EC5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A970F5C2C435D170043EC5E /* Store.swift in Sources */, + 9ADC410B2C43862600D8500A /* CounterReducer.swift in Sources */, + 9A970F332C435CEB0043EC5E /* ContentView.swift in Sources */, + 9ADC41162C43C9F100D8500A /* AddTaskView.swift in Sources */, + 9A970F312C435CEB0043EC5E /* SwiftUIReduxApp.swift in Sources */, + 9ADC41142C43C30B00D8500A /* TaskReducer.swift in Sources */, + 9ADC41122C43C27000D8500A /* Task.swift in Sources */, + 9ADC410F2C4386A400D8500A /* AppReducer.swift in Sources */, + 9ADC41192C43D59800D8500A /* LogMiddleware.swift in Sources */, + 9ADC411B2C43D88E00D8500A /* IncrementMiddleware.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A970F392C435CED0043EC5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A970F422C435CED0043EC5E /* SwiftUIReduxTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A970F432C435CED0043EC5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A970F4C2C435CED0043EC5E /* SwiftUIReduxUITests.swift in Sources */, + 9A970F4E2C435CED0043EC5E /* SwiftUIReduxUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 9A970F3F2C435CED0043EC5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A970F2C2C435CEB0043EC5E /* SwiftUIRedux */; + targetProxy = 9A970F3E2C435CED0043EC5E /* PBXContainerItemProxy */; + }; + 9A970F492C435CED0043EC5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A970F2C2C435CEB0043EC5E /* SwiftUIRedux */; + targetProxy = 9A970F482C435CED0043EC5E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 9A970F4F2C435CED0043EC5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 9A970F502C435CED0043EC5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 9A970F522C435CED0043EC5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SwiftUIRedux/Preview Content\""; + DEVELOPMENT_TEAM = A34NNDTR9F; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chenyilong.SwiftUIRedux; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9A970F532C435CED0043EC5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SwiftUIRedux/Preview Content\""; + DEVELOPMENT_TEAM = A34NNDTR9F; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chenyilong.SwiftUIRedux; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 9A970F552C435CED0043EC5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A34NNDTR9F; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chenyilong.SwiftUIReduxTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUIRedux.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftUIRedux"; + }; + name = Debug; + }; + 9A970F562C435CED0043EC5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A34NNDTR9F; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chenyilong.SwiftUIReduxTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUIRedux.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftUIRedux"; + }; + name = Release; + }; + 9A970F582C435CED0043EC5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A34NNDTR9F; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chenyilong.SwiftUIReduxUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SwiftUIRedux; + }; + name = Debug; + }; + 9A970F592C435CED0043EC5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A34NNDTR9F; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chenyilong.SwiftUIReduxUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SwiftUIRedux; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9A970F282C435CEB0043EC5E /* Build configuration list for PBXProject "SwiftUIRedux" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A970F4F2C435CED0043EC5E /* Debug */, + 9A970F502C435CED0043EC5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9A970F512C435CED0043EC5E /* Build configuration list for PBXNativeTarget "SwiftUIRedux" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A970F522C435CED0043EC5E /* Debug */, + 9A970F532C435CED0043EC5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9A970F542C435CED0043EC5E /* Build configuration list for PBXNativeTarget "SwiftUIReduxTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A970F552C435CED0043EC5E /* Debug */, + 9A970F562C435CED0043EC5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9A970F572C435CED0043EC5E /* Build configuration list for PBXNativeTarget "SwiftUIReduxUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A970F582C435CED0043EC5E /* Debug */, + 9A970F592C435CED0043EC5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9A970F252C435CEB0043EC5E /* Project object */; +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/AddTaskView.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/AddTaskView.swift new file mode 100644 index 0000000..49b3130 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/AddTaskView.swift @@ -0,0 +1,62 @@ +// +// AddTaskView.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import SwiftUI + +struct AddTaskView: View { + @EnvironmentObject var store: Store + @State private var name: String = "" + + struct Props { + let tasks: [Task] + let onTaskAdded: (Task) -> () + } + + private func map(state: TaskState) -> Props { + return Props(tasks: state.tasks, + onTaskAdded: { task in + store.dispatch(action: AddTaskAction(task: task)) + }) + } + + var body: some View { + let props = map(state: store.state.taskState) + + VStack { + TextField("Enter task", text: $name) + .padding() + .background(Color(.systemGray6)) + .cornerRadius(10) + .padding() + + Button(action: { + let task = Task(title: self.name) + props.onTaskAdded(task) + // dispatch action +// print("Add Task\(name)"); + }) { + Text("Add Task") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + + List(props.tasks, id: \.id) { task in + Text(task.title) + } + .padding() + } + } +} + +struct AddTaskView_Previews: PreviewProvider { + static var previews: some View { + let store = Store(reducer: appReducer, state: AppState()) + return AddTaskView().environmentObject(store) + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/ContentView.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/ContentView.swift new file mode 100644 index 0000000..19982ad --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/ContentView.swift @@ -0,0 +1,89 @@ +// +// ContentView.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import SwiftUI + +struct ContentView: View { + @State private var isPresented: Bool = false + + @EnvironmentObject var store: Store + + struct Props { + let counter: Int + let onIncrement: () -> Void + let onDecrement: () -> Void + let onAdd: (Int) -> Void + let onIncrementAsync: () -> Void + } + + private func map(state: CounterState) -> Props { + return Props( + counter: state.counter, + onIncrement: { store.dispatch(action: IncrementAction()) }, + onDecrement: { store.dispatch(action: DecrementAction()) }, + onAdd: { value in store.dispatch(action: addAction(value: value)) }, + onIncrementAsync: { store.dispatch(action: IncrementActionAsync()) } + ) + } + + + var body: some View { + + let props = map(state: store.state.counterState) + + VStack { + Spacer() + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + Text("\(props.counter)") + .font(.largeTitle) + Button(action: { + props.onIncrement() + }) { + Text("Increment") + } + + Button(action: { + props.onDecrement() + }) { + Text("Decrement") + } + + Button(action: { + props.onAdd(10) + }) { + Text("Add 10") + } + + Button(action: { + props.onIncrementAsync() + }) { + Text("onIncrementAsync") + } + + Spacer() + + Button(action: { + isPresented = true + }) { + Text("Add Tasks") + } + } + .padding().sheet(isPresented: $isPresented) { + Text("Add text view") + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { +// let store = Store(reducer: appReducer, state: AppState(), middlewares: [logMiddleware(), incrementMiddleware()]) + let store = Store(reducer: counterReducer, state: CounterState()) + ContentView().environmentObject(store) + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Models/Task.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Models/Task.swift new file mode 100644 index 0000000..6b128b9 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Models/Task.swift @@ -0,0 +1,13 @@ +// +// Task.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +struct Task { + let id = UUID() + let title: String +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Preview Content/Preview Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/Store.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/Store.swift new file mode 100644 index 0000000..1772924 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/Store.swift @@ -0,0 +1,74 @@ +// +// Store.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +/*! + * Generics in Swift and Dependency Injection are closely related when it comes to implementing the Dependency Inversion Principle (DIP). Generics provide the necessary abstraction to decouple high-level modules from low-level modules, while dependency injection ensures that dependencies are injected from the outside, promoting modularity and testability. In the provided `Store` class example, generics and constructor injection work together to create a flexible, reusable, and decoupled state management system. + */ +typealias Dispatcher = (Action) -> Void +typealias Reducer = (_ state: State,_ action: Action) -> State +typealias Middleware = (StoreState, Action, @escaping Dispatcher) -> Void + +protocol ReduxState { } + +struct AppState: ReduxState { + var counterState = CounterState() + var taskState = TaskState() +} + +struct CounterState: ReduxState { + var counter: Int = 0 + var movies = [String]() +} + +struct TaskState: ReduxState { + var tasks: [Task] = [Task]() +} + +protocol Action { } + +struct IncrementAction: Action { } +struct DecrementAction: Action { } +struct IncrementActionAsync: Action { } + +struct getMoviesAction: Action { + let moveis: [String] +} + +struct addAction: Action { + var value: Int +} + +struct AddTaskAction: Action { + var task: Task +} + +class Store: ObservableObject { + + var reducer: Reducer + @Published var state: StoreState + var middlewares: [Middleware] = [] + + init(reducer:@escaping Reducer, state: StoreState, middlewares: [Middleware] = []) { + self.state = state + self.reducer = reducer + self.middlewares = middlewares + } + + func dispatch(action: Action) { + DispatchQueue.main.async { + self.state = self.reducer(self.state, action) + print("reducer update state") + } + + middlewares.forEach { middleware in + print("middlewares.forEach") + middleware(state, action, self.dispatch) + } + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/middlewares/IncrementMiddleware.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/middlewares/IncrementMiddleware.swift new file mode 100644 index 0000000..18dfe69 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/middlewares/IncrementMiddleware.swift @@ -0,0 +1,22 @@ +// +// IncrementMiddleware.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +func incrementMiddleware() -> Middleware { + return { state, action, dispatch in + switch action { + case _ as IncrementActionAsync: + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + dispatch(IncrementAction()) + print("dispatch IncrementActionAsync") + } + default: + break + } + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/middlewares/LogMiddleware.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/middlewares/LogMiddleware.swift new file mode 100644 index 0000000..30e11bc --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/middlewares/LogMiddleware.swift @@ -0,0 +1,14 @@ +// +// LogMiddleware.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +func logMiddleware() -> Middleware { + return { state, action, dispatch in + print("logMiddleware Action trigger: \(action)") + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/AppReducer.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/AppReducer.swift new file mode 100644 index 0000000..e9816e0 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/AppReducer.swift @@ -0,0 +1,15 @@ +// +// AppReducer.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +func appReducer(_ state: AppState,_ action: Action) -> AppState { + var state = state + state.counterState = counterReducer(state.counterState, action) + state.taskState = taskReducer(state.taskState, action) + return state +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/CounterReducer.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/CounterReducer.swift new file mode 100644 index 0000000..6fdcf3b --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/CounterReducer.swift @@ -0,0 +1,26 @@ +// +// CounterReducer.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +func counterReducer(_ state: CounterState,_ action: Action) -> CounterState { + var state = state + switch action { + case _ as IncrementAction: + state.counter += 1 + case _ as DecrementAction: + state.counter -= 1 + case let action as addAction: + state.counter += action.value + case let action as getMoviesAction: + state.movies = action.moveis + default: + break + } + + return state +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/TaskReducer.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/TaskReducer.swift new file mode 100644 index 0000000..824b713 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/Store/reducers/TaskReducer.swift @@ -0,0 +1,20 @@ +// +// TaskReducer.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import Foundation + +func taskReducer(_ state: TaskState,_ action: Action) -> TaskState { + var state = state + switch action { + case let action as AddTaskAction: + state.tasks.append(action.task) + default: + break + } + + return state +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIRedux/SwiftUIReduxApp.swift b/SwiftUI/SwiftUIRedux/SwiftUIRedux/SwiftUIReduxApp.swift new file mode 100644 index 0000000..527ddcb --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIRedux/SwiftUIReduxApp.swift @@ -0,0 +1,19 @@ +// +// SwiftUIReduxApp.swift +// SwiftUIRedux +// +// Created by chenyilong on 2024/7/14. +// + +import SwiftUI + +@main +struct SwiftUIReduxApp: App { + var body: some Scene { + let store = Store(reducer: appReducer, state: AppState(), middlewares: [logMiddleware(), incrementMiddleware()]) + WindowGroup { + ContentView().environmentObject(store) +// AddTaskView().environmentObject(store) + } + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIReduxTests/SwiftUIReduxTests.swift b/SwiftUI/SwiftUIRedux/SwiftUIReduxTests/SwiftUIReduxTests.swift new file mode 100644 index 0000000..b3e0a4a --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIReduxTests/SwiftUIReduxTests.swift @@ -0,0 +1,36 @@ +// +// SwiftUIReduxTests.swift +// SwiftUIReduxTests +// +// Created by chenyilong on 2024/7/14. +// + +import XCTest +@testable import SwiftUIRedux + +final class SwiftUIReduxTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIReduxUITests/SwiftUIReduxUITests.swift b/SwiftUI/SwiftUIRedux/SwiftUIReduxUITests/SwiftUIReduxUITests.swift new file mode 100644 index 0000000..7764abb --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIReduxUITests/SwiftUIReduxUITests.swift @@ -0,0 +1,41 @@ +// +// SwiftUIReduxUITests.swift +// SwiftUIReduxUITests +// +// Created by chenyilong on 2024/7/14. +// + +import XCTest + +final class SwiftUIReduxUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/SwiftUI/SwiftUIRedux/SwiftUIReduxUITests/SwiftUIReduxUITestsLaunchTests.swift b/SwiftUI/SwiftUIRedux/SwiftUIReduxUITests/SwiftUIReduxUITestsLaunchTests.swift new file mode 100644 index 0000000..bd07be8 --- /dev/null +++ b/SwiftUI/SwiftUIRedux/SwiftUIReduxUITests/SwiftUIReduxUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// SwiftUIReduxUITestsLaunchTests.swift +// SwiftUIReduxUITests +// +// Created by chenyilong on 2024/7/14. +// + +import XCTest + +final class SwiftUIReduxUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/ReduxAsync.xcodeproj/project.pbxproj b/SwiftUI/SwiftUIRedux_MovieApp/ReduxAsync.xcodeproj/project.pbxproj new file mode 100755 index 0000000..237de0a --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/ReduxAsync.xcodeproj/project.pbxproj @@ -0,0 +1,606 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 340AF81F2514033B00D0E6B7 /* URLImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340AF81E2514033B00D0E6B7 /* URLImage.swift */; }; + 340AF8202514033B00D0E6B7 /* URLImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340AF81E2514033B00D0E6B7 /* URLImage.swift */; }; + 340AF8242514034700D0E6B7 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340AF8232514034700D0E6B7 /* ImageLoader.swift */; }; + 340AF8252514034700D0E6B7 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340AF8232514034700D0E6B7 /* ImageLoader.swift */; }; + 34111D5D250FFE8C009064B8 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34111D5C250FFE8C009064B8 /* Store.swift */; }; + 34111D5E250FFE8C009064B8 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34111D5C250FFE8C009064B8 /* Store.swift */; }; + 341B4AC325153E4C007BB846 /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341B4AC225153E4B007BB846 /* Movie.swift */; }; + 341B4AC425153E4C007BB846 /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341B4AC225153E4B007BB846 /* Movie.swift */; }; + 342978D3250FEC1500F20610 /* HelloReduxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342978C0250FEC1300F20610 /* HelloReduxApp.swift */; }; + 342978D4250FEC1500F20610 /* HelloReduxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342978C0250FEC1300F20610 /* HelloReduxApp.swift */; }; + 342978D5250FEC1500F20610 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342978C1250FEC1300F20610 /* ContentView.swift */; }; + 342978D6250FEC1500F20610 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342978C1250FEC1300F20610 /* ContentView.swift */; }; + 342978D7250FEC1500F20610 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 342978C2250FEC1500F20610 /* Assets.xcassets */; }; + 342978D8250FEC1500F20610 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 342978C2250FEC1500F20610 /* Assets.xcassets */; }; + 343CB8BB25193AF4008E9A7B /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343CB8BA25193AF4008E9A7B /* View+Extensions.swift */; }; + 343CB8BC25193AF4008E9A7B /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343CB8BA25193AF4008E9A7B /* View+Extensions.swift */; }; + 34BBFFC7251531B4002E9388 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBFFC6251531B4002E9388 /* Constants.swift */; }; + 34BBFFC8251531B4002E9388 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBFFC6251531B4002E9388 /* Constants.swift */; }; + 34D075DB25191A4A00DBA6F7 /* MoviesReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D075DA25191A4A00DBA6F7 /* MoviesReducer.swift */; }; + 34D075DC25191A4A00DBA6F7 /* MoviesReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D075DA25191A4A00DBA6F7 /* MoviesReducer.swift */; }; + 34D075ED25191C2F00DBA6F7 /* Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D075EC25191C2F00DBA6F7 /* Webservice.swift */; }; + 34D075EE25191C2F00DBA6F7 /* Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D075EC25191C2F00DBA6F7 /* Webservice.swift */; }; + 34D2F6F4251150DC00117401 /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2F6F3251150DC00117401 /* AppReducer.swift */; }; + 34D2F6F5251150DC00117401 /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2F6F3251150DC00117401 /* AppReducer.swift */; }; + 9A11191E2C4D018A005C31F4 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11191D2C4D018A005C31F4 /* String+Extensions.swift */; }; + 9A11191F2C4D018A005C31F4 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11191D2C4D018A005C31F4 /* String+Extensions.swift */; }; + 9A1119212C4D04EF005C31F4 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119202C4D04EF005C31F4 /* RatingView.swift */; }; + 9A1119222C4D04EF005C31F4 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119202C4D04EF005C31F4 /* RatingView.swift */; }; + 9A1119272C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */; }; + 9A1119282C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */; }; + 9A11192A2C4D19CC005C31F4 /* MovieDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119292C4D19CC005C31F4 /* MovieDetail.swift */; }; + 9A11192B2C4D19CC005C31F4 /* MovieDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119292C4D19CC005C31F4 /* MovieDetail.swift */; }; + 9A11192D2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */; }; + 9A11192E2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 340AF81E2514033B00D0E6B7 /* URLImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLImage.swift; sourceTree = ""; }; + 340AF8232514034700D0E6B7 /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; + 34111D5C250FFE8C009064B8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 341B4AC225153E4B007BB846 /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; + 342978C0250FEC1300F20610 /* HelloReduxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloReduxApp.swift; sourceTree = ""; }; + 342978C1250FEC1300F20610 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 342978C2250FEC1500F20610 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 342978C7250FEC1500F20610 /* ReduxAsync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReduxAsync.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 342978CA250FEC1500F20610 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 342978CF250FEC1500F20610 /* ReduxAsync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReduxAsync.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 342978D1250FEC1500F20610 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 342978D2250FEC1500F20610 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; + 343CB8BA25193AF4008E9A7B /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; + 34BBFFC6251531B4002E9388 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 34D075DA25191A4A00DBA6F7 /* MoviesReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesReducer.swift; sourceTree = ""; }; + 34D075EC25191C2F00DBA6F7 /* Webservice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Webservice.swift; sourceTree = ""; }; + 34D2F6F3251150DC00117401 /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; + 9A11191D2C4D018A005C31F4 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 9A1119202C4D04EF005C31F4 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = ""; }; + 9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesMiddleware.swift; sourceTree = ""; }; + 9A1119292C4D19CC005C31F4 /* MovieDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetail.swift; sourceTree = ""; }; + 9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 342978C4250FEC1500F20610 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 342978CC250FEC1500F20610 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 340AF81C2513FEC300D0E6B7 /* Views */ = { + isa = PBXGroup; + children = ( + 340AF81E2514033B00D0E6B7 /* URLImage.swift */, + 9A1119202C4D04EF005C31F4 /* RatingView.swift */, + 342978C1250FEC1300F20610 /* ContentView.swift */, + 9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 340AF81D2514032D00D0E6B7 /* Loaders */ = { + isa = PBXGroup; + children = ( + 340AF8232514034700D0E6B7 /* ImageLoader.swift */, + ); + path = Loaders; + sourceTree = ""; + }; + 34111D5B250FFE80009064B8 /* Store */ = { + isa = PBXGroup; + children = ( + 9A1119232C4D0F3A005C31F4 /* Middlewares */, + 34D2F6EF2511504000117401 /* reducers */, + 34111D5C250FFE8C009064B8 /* Store.swift */, + ); + path = Store; + sourceTree = ""; + }; + 342978BA250FEC1200F20610 = { + isa = PBXGroup; + children = ( + 342978BF250FEC1300F20610 /* Shared */, + 342978C9250FEC1500F20610 /* iOS */, + 342978D0250FEC1500F20610 /* macOS */, + 342978C8250FEC1500F20610 /* Products */, + ); + sourceTree = ""; + }; + 342978BF250FEC1300F20610 /* Shared */ = { + isa = PBXGroup; + children = ( + 343CB8B925193AD7008E9A7B /* Extensions */, + 34D075E925191C1E00DBA6F7 /* Services */, + 34BBFFC5251531A1002E9388 /* Utils */, + 340AF81D2514032D00D0E6B7 /* Loaders */, + 340AF81C2513FEC300D0E6B7 /* Views */, + 346D49BC25123D1100C27E0E /* Models */, + 34111D5B250FFE80009064B8 /* Store */, + 342978C0250FEC1300F20610 /* HelloReduxApp.swift */, + 342978C2250FEC1500F20610 /* Assets.xcassets */, + ); + path = Shared; + sourceTree = ""; + }; + 342978C8250FEC1500F20610 /* Products */ = { + isa = PBXGroup; + children = ( + 342978C7250FEC1500F20610 /* ReduxAsync.app */, + 342978CF250FEC1500F20610 /* ReduxAsync.app */, + ); + name = Products; + sourceTree = ""; + }; + 342978C9250FEC1500F20610 /* iOS */ = { + isa = PBXGroup; + children = ( + 342978CA250FEC1500F20610 /* Info.plist */, + ); + path = iOS; + sourceTree = ""; + }; + 342978D0250FEC1500F20610 /* macOS */ = { + isa = PBXGroup; + children = ( + 342978D1250FEC1500F20610 /* Info.plist */, + 342978D2250FEC1500F20610 /* macOS.entitlements */, + ); + path = macOS; + sourceTree = ""; + }; + 343CB8B925193AD7008E9A7B /* Extensions */ = { + isa = PBXGroup; + children = ( + 343CB8BA25193AF4008E9A7B /* View+Extensions.swift */, + 9A11191D2C4D018A005C31F4 /* String+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 346D49BC25123D1100C27E0E /* Models */ = { + isa = PBXGroup; + children = ( + 341B4AC225153E4B007BB846 /* Movie.swift */, + 9A1119292C4D19CC005C31F4 /* MovieDetail.swift */, + ); + path = Models; + sourceTree = ""; + }; + 34BBFFC5251531A1002E9388 /* Utils */ = { + isa = PBXGroup; + children = ( + 34BBFFC6251531B4002E9388 /* Constants.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 34D075E925191C1E00DBA6F7 /* Services */ = { + isa = PBXGroup; + children = ( + 34D075EC25191C2F00DBA6F7 /* Webservice.swift */, + ); + path = Services; + sourceTree = ""; + }; + 34D2F6EF2511504000117401 /* reducers */ = { + isa = PBXGroup; + children = ( + 34D2F6F3251150DC00117401 /* AppReducer.swift */, + 34D075DA25191A4A00DBA6F7 /* MoviesReducer.swift */, + ); + path = reducers; + sourceTree = ""; + }; + 9A1119232C4D0F3A005C31F4 /* Middlewares */ = { + isa = PBXGroup; + children = ( + 9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */, + ); + path = Middlewares; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 342978C6250FEC1500F20610 /* ReduxAsync (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 342978DB250FEC1500F20610 /* Build configuration list for PBXNativeTarget "ReduxAsync (iOS)" */; + buildPhases = ( + 342978C3250FEC1500F20610 /* Sources */, + 342978C4250FEC1500F20610 /* Frameworks */, + 342978C5250FEC1500F20610 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ReduxAsync (iOS)"; + productName = "HelloRedux (iOS)"; + productReference = 342978C7250FEC1500F20610 /* ReduxAsync.app */; + productType = "com.apple.product-type.application"; + }; + 342978CE250FEC1500F20610 /* ReduxAsync (macOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 342978DE250FEC1500F20610 /* Build configuration list for PBXNativeTarget "ReduxAsync (macOS)" */; + buildPhases = ( + 342978CB250FEC1500F20610 /* Sources */, + 342978CC250FEC1500F20610 /* Frameworks */, + 342978CD250FEC1500F20610 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ReduxAsync (macOS)"; + productName = "HelloRedux (macOS)"; + productReference = 342978CF250FEC1500F20610 /* ReduxAsync.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 342978BB250FEC1200F20610 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1420; + TargetAttributes = { + 342978C6250FEC1500F20610 = { + CreatedOnToolsVersion = 12.0; + }; + 342978CE250FEC1500F20610 = { + CreatedOnToolsVersion = 12.0; + }; + }; + }; + buildConfigurationList = 342978BE250FEC1200F20610 /* Build configuration list for PBXProject "ReduxAsync" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 342978BA250FEC1200F20610; + productRefGroup = 342978C8250FEC1500F20610 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 342978C6250FEC1500F20610 /* ReduxAsync (iOS) */, + 342978CE250FEC1500F20610 /* ReduxAsync (macOS) */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 342978C5250FEC1500F20610 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 342978D7250FEC1500F20610 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 342978CD250FEC1500F20610 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 342978D8250FEC1500F20610 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 342978C3250FEC1500F20610 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A1119272C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */, + 340AF8242514034700D0E6B7 /* ImageLoader.swift in Sources */, + 9A11192D2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */, + 34111D5D250FFE8C009064B8 /* Store.swift in Sources */, + 343CB8BB25193AF4008E9A7B /* View+Extensions.swift in Sources */, + 340AF81F2514033B00D0E6B7 /* URLImage.swift in Sources */, + 9A11191E2C4D018A005C31F4 /* String+Extensions.swift in Sources */, + 342978D5250FEC1500F20610 /* ContentView.swift in Sources */, + 34BBFFC7251531B4002E9388 /* Constants.swift in Sources */, + 9A1119212C4D04EF005C31F4 /* RatingView.swift in Sources */, + 342978D3250FEC1500F20610 /* HelloReduxApp.swift in Sources */, + 341B4AC325153E4C007BB846 /* Movie.swift in Sources */, + 34D2F6F4251150DC00117401 /* AppReducer.swift in Sources */, + 9A11192A2C4D19CC005C31F4 /* MovieDetail.swift in Sources */, + 34D075ED25191C2F00DBA6F7 /* Webservice.swift in Sources */, + 34D075DB25191A4A00DBA6F7 /* MoviesReducer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 342978CB250FEC1500F20610 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A1119282C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */, + 340AF8252514034700D0E6B7 /* ImageLoader.swift in Sources */, + 9A11192E2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */, + 34111D5E250FFE8C009064B8 /* Store.swift in Sources */, + 343CB8BC25193AF4008E9A7B /* View+Extensions.swift in Sources */, + 340AF8202514033B00D0E6B7 /* URLImage.swift in Sources */, + 9A11191F2C4D018A005C31F4 /* String+Extensions.swift in Sources */, + 342978D6250FEC1500F20610 /* ContentView.swift in Sources */, + 34BBFFC8251531B4002E9388 /* Constants.swift in Sources */, + 9A1119222C4D04EF005C31F4 /* RatingView.swift in Sources */, + 342978D4250FEC1500F20610 /* HelloReduxApp.swift in Sources */, + 341B4AC425153E4C007BB846 /* Movie.swift in Sources */, + 34D2F6F5251150DC00117401 /* AppReducer.swift in Sources */, + 9A11192B2C4D19CC005C31F4 /* MovieDetail.swift in Sources */, + 34D075EE25191C2F00DBA6F7 /* Webservice.swift in Sources */, + 34D075DC25191A4A00DBA6F7 /* MoviesReducer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 342978D9250FEC1500F20610 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 342978DA250FEC1500F20610 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 342978DC250FEC1500F20610 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iOS/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloRedux; + PRODUCT_NAME = ReduxAsync; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 342978DD250FEC1500F20610 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iOS/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloRedux; + PRODUCT_NAME = ReduxAsync; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 342978DF250FEC1500F20610 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloRedux; + PRODUCT_NAME = ReduxAsync; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 342978E0250FEC1500F20610 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloRedux; + PRODUCT_NAME = ReduxAsync; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 342978BE250FEC1200F20610 /* Build configuration list for PBXProject "ReduxAsync" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 342978D9250FEC1500F20610 /* Debug */, + 342978DA250FEC1500F20610 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 342978DB250FEC1500F20610 /* Build configuration list for PBXNativeTarget "ReduxAsync (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 342978DC250FEC1500F20610 /* Debug */, + 342978DD250FEC1500F20610 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 342978DE250FEC1500F20610 /* Build configuration list for PBXNativeTarget "ReduxAsync (macOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 342978DF250FEC1500F20610 /* Debug */, + 342978E0250FEC1500F20610 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 342978BB250FEC1200F20610 /* Project object */; +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100755 index 0000000..eb87897 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..c136eaf --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,148 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/placeholder.imageset/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/placeholder.imageset/Contents.json new file mode 100755 index 0000000..d7a6ec8 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/placeholder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "placeholder.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/placeholder.imageset/placeholder.png b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/placeholder.imageset/placeholder.png new file mode 100755 index 0000000..87214dd Binary files /dev/null and b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Assets.xcassets/placeholder.imageset/placeholder.png differ diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Extensions/String+Extensions.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Extensions/String+Extensions.swift new file mode 100644 index 0000000..5345fab --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Extensions/String+Extensions.swift @@ -0,0 +1,20 @@ +// +// String+Extensions.swift +// ReduxAsync +// +// Created by chenyilong on 2024/7/21. +// + +import Foundation +import SwiftUI + +extension String { + func urlEncode() -> String { + self.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self + } + + func toInt() -> Int { + let ratingDouble = Double(self) ?? 0 + return Int(ratingDouble.rounded()) + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Extensions/View+Extensions.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Extensions/View+Extensions.swift new file mode 100755 index 0000000..4db6c4a --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Extensions/View+Extensions.swift @@ -0,0 +1,17 @@ +// +// View+Extensions.swift +// ReduxAsync +// +// Created by Yilong Chen on 9/21/24. +// + +import Foundation +import SwiftUI + +extension View { + + func embedInNavigationView() -> some View { + NavigationView { self } + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/HelloReduxApp.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/HelloReduxApp.swift new file mode 100755 index 0000000..ef5a00f --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/HelloReduxApp.swift @@ -0,0 +1,21 @@ +// +// HelloReduxApp.swift +// Shared +// +// Created by Yilong Chen on 9/14/24. +// + +import SwiftUI + +@main +struct HelloReduxApp: App { + var body: some Scene { + + let store = Store(reducer: appReducer, state: AppState(), + middlewares: [moviesMiddleware()]) + + WindowGroup { + ContentView().environmentObject(store) + } + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Loaders/ImageLoader.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Loaders/ImageLoader.swift new file mode 100755 index 0000000..0e3f03a --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Loaders/ImageLoader.swift @@ -0,0 +1,35 @@ +// +// ImageLoader.swift +// URLImageDemo +// +// Created by Yilong Chen on 6/17/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import Foundation + +class ImageLoader: ObservableObject { + + @Published var downloadedData: Data? = nil + + func downloadImage(url: String) { + + guard let imageURL = URL(string: url) else { + fatalError("ImageURL is not correct") + } + + URLSession.shared.dataTask(with: imageURL) { data, _, error in + + guard let data = data, error == nil else { + return + } + + DispatchQueue.main.async { + self.downloadedData = data + } + + }.resume() + + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Models/Movie.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Models/Movie.swift new file mode 100755 index 0000000..bafa359 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Models/Movie.swift @@ -0,0 +1,31 @@ +// +// Movie.swift +// ReduxAsync +// +// Created by Yilong Chen on 9/18/24. +// + +import Foundation + +struct MovieResponse: Codable { + let movies: [Movie] + + private enum CodingKeys: String, CodingKey { + case movies = "Search" + } +} + + +struct Movie: Codable { + + let title: String + let imdbId: String + let poster: String + + private enum CodingKeys: String, CodingKey { + case title = "Title" + case imdbId = "imdbID" + case poster = "Poster" + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Models/MovieDetail.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Models/MovieDetail.swift new file mode 100644 index 0000000..0416fee --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Models/MovieDetail.swift @@ -0,0 +1,75 @@ +// +// MovieDetail.swift +// ReduxAsync +// +// Created by chenyilong on 2024/7/21. +// + +import Foundation + +struct Rating: Decodable { + let source: String + let value: String + + private enum CodingKeys: String, CodingKey { + case source = "Source" + case value = "Value" + } +} + +struct MovieDetail: Decodable { + let title: String + let year: String + let rated: String + let released: String + let runtime: String + let genre: String + let director: String + let writer: String + let actors: String + let plot: String + let language: String + let country: String + let awards: String + let poster: String + let ratings: [Rating] + let metascore: String + let imdbRating: String + let imdbVotes: String + let imdbId: String + let type: String + let dvd: String + let boxOffice: String + let production: String + let website: String + let response: String + + private enum CodingKeys: String, CodingKey { + case title = "Title" + case year = "Year" + case rated = "Rated" + case released = "Released" + case runtime = "Runtime" + case genre = "Genre" + case director = "Director" + case writer = "Writer" + case actors = "Actors" + case plot = "Plot" + case language = "Language" + case country = "Country" + case awards = "Awards" + case poster = "Poster" + case ratings = "Ratings" + case metascore = "Metascore" + case imdbRating = "imdbRating" + case imdbVotes = "imdbVotes" + case imdbId = "imdbID" + case type = "Type" + case dvd = "DVD" + case boxOffice = "BoxOffice" + case production = "Production" + case website = "Website" + case response = "Response" + } +} + diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Services/Webservice.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Services/Webservice.swift new file mode 100755 index 0000000..01bd55b --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Services/Webservice.swift @@ -0,0 +1,110 @@ +// +// Webservice.swift +// ReduxAsync +// +// Created by Yilong Chen on 9/21/24. +// + +import Foundation + +enum NetworkError: Error { + case badUrl + case decodingError + case noData + case networkError(Error) +} + +class Webservice { + func getMovieDetailsById(id: String, completion: @escaping (Result) -> Void) { + + let url = URL(string: Constants.Url.urlForMovieDetailsByImdbId(id)) + + guard let movieUrl = url else { + completion(.failure(.badUrl)) + return + } + + URLSession.shared.dataTask(with: movieUrl) { data, _, error in + + if let error = error { + completion(.failure(.networkError(error))) + return + } + + guard let data = data else { + completion(.failure(.noData)) + return + } + + do { + let movieDetail = try JSONDecoder().decode(MovieDetail.self, from: data) + completion(.success(movieDetail)) + } catch { + completion(.failure(.decodingError)) + } + + }.resume() + } + + func getMoviesBySearch(search: String, completion: @escaping (Result<[Movie]?, NetworkError>) -> Void) { + + let search = search.replacingOccurrences(of: " ", with: "+") + let url = URL(string: Constants.Url.urlBySearch(search)) + + guard let moviesUrl = url else { + completion(.failure(.badUrl)) + return + } + + URLSession.shared.dataTask(with: moviesUrl) { data, _, error in + + if let error = error { + completion(.failure(.networkError(error))) + return + } + + guard let data = data else { + completion(.failure(.noData)) + return + } + + do { + let movieResponse = try JSONDecoder().decode(MovieResponse.self, from: data) + completion(.success(movieResponse.movies)) + } catch { + completion(.failure(.decodingError)) + } + + }.resume() + } + + func getAllMovies(url: String, completion: @escaping (Result<[Movie], NetworkError>) -> Void) { + + guard let moviesUrl = URL(string: url) else { + completion(.failure(.badUrl)) + return + } + + URLSession.shared.dataTask(with: moviesUrl) { data, _, error in + + if let error = error { + completion(.failure(.networkError(error))) + return + } + + guard let data = data else { + completion(.failure(.noData)) + return + } + + do { + let movieResponse = try JSONDecoder().decode(MovieResponse.self, from: data) + completion(.success(movieResponse.movies)) + } catch { + completion(.failure(.decodingError)) + } + + }.resume() + + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Middlewares/MoviesMiddleware.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Middlewares/MoviesMiddleware.swift new file mode 100644 index 0000000..621e7a4 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Middlewares/MoviesMiddleware.swift @@ -0,0 +1,47 @@ +// +// MoviesMiddleware.swift +// ReduxAsync +// +// Created by chenyilong on 2024/7/21. +// + +import Foundation + +// Middlewares +func moviesMiddleware() -> Middleware { + + return { state, action, dispatch in + + switch action { + case let action as FetchMoviesAction: + Webservice().getMoviesBySearch(search: action.search.urlEncode()) { result in + switch result { + case .success(let movies): + if let movies = movies { + print(movies) + dispatch(SetMoviesAction(movies: movies)) + } + + case .failure(let error): + print(error.localizedDescription) + } + } + case let action as FetchMovieDetailAction: + Webservice().getMovieDetailsById(id: action.imdbId) { result in + switch result { + case .success(let movieDetail): + if let movieDetail = movieDetail { + dispatch(SetMovieDetailAction(movieDetail: movieDetail)) + } + + case .failure(let error): + print(error.localizedDescription) + } + } + default: + break + } + + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Store.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Store.swift new file mode 100755 index 0000000..5d1bc38 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Store.swift @@ -0,0 +1,69 @@ +// +// Store.swift +// HelloRedux +// +// Created by Yilong Chen on 9/14/24. +// + +import Foundation + +typealias Dispatcher = (Action) -> Void + +typealias Reducer = (_ state: State, _ action: Action) -> State +typealias Middleware = (StoreState, Action, @escaping Dispatcher) -> Void + +protocol ReduxState { } + +struct AppState: ReduxState { + var moviesState = MoviesState() +} + +struct MoviesState: ReduxState { + var movies: [Movie] = [Movie]() + var selectedMovieDetail: MovieDetail? +} + +protocol Action { } + +struct FetchMoviesAction: Action { + let search: String +} + +struct SetMoviesAction: Action { + let movies: [Movie] +} + +struct FetchMovieDetailAction: Action { + let imdbId: String +} + +struct SetMovieDetailAction: Action { + let movieDetail: MovieDetail +} + +class Store: ObservableObject { + + var reducer: Reducer + @Published var state: StoreState + var middlewares: [Middleware] + + init(reducer: @escaping Reducer, state: StoreState + , middlewares: [Middleware] = [] + ) { + self.reducer = reducer + self.state = state + self.middlewares = middlewares + } + + func dispatch(action: Action) { + DispatchQueue.main.async { + self.state = self.reducer(self.state, action) + } + + // run all the middlewares + middlewares.forEach { middleware in + middleware(state, action, dispatch) + } + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/AppReducer.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/AppReducer.swift new file mode 100755 index 0000000..a69d124 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/AppReducer.swift @@ -0,0 +1,15 @@ +// +// AppReducer.swift +// CombiningReducers +// +// Created by Yilong Chen on 9/15/24. +// + +import Foundation + +func appReducer(_ state: AppState, _ action: Action) -> AppState { + + var state = state + state.moviesState = moviesReducer(state: state.moviesState, action: action) + return state +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/MoviesReducer.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/MoviesReducer.swift new file mode 100755 index 0000000..8e033f6 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/MoviesReducer.swift @@ -0,0 +1,24 @@ +// +// MoviesReducer.swift +// ReduxAsync +// +// Created by Yilong Chen on 9/21/24. +// + +import Foundation + +func moviesReducer(state: MoviesState, action: Action) -> MoviesState { + + var state = state + + switch action { + case let action as SetMoviesAction: + state.movies = action.movies + case let action as SetMovieDetailAction: + state.selectedMovieDetail = action.movieDetail + default: + break + } + + return state +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Utils/Constants.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Utils/Constants.swift new file mode 100755 index 0000000..0234fb0 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Utils/Constants.swift @@ -0,0 +1,25 @@ +// +// Constants.swift +// ReduxAsync +// +// Created by Yilong Chen on 9/18/24. +// + +import Foundation + +struct Constants { + struct api { + static let key = "564727fa" + } + + struct Url { + static func urlBySearch(_ search: String) -> String { + return "http://www.omdbapi.com/?s=\(search)&page=1&apikey=\(api.key)" + } + static func urlForMovieDetailsByImdbId(_ imdbId: String) -> String { + return "http://www.omdbapi.com/?i=\(imdbId)&apikey=\(api.key)" + } + static let moviesURL = "http://www.omdbapi.com/?s=Batman&page=2&apikey=\(api.key)" + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/ContentView.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/ContentView.swift new file mode 100755 index 0000000..8609959 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/ContentView.swift @@ -0,0 +1,64 @@ +// +// ContentView.swift +// Shared +// +// Created by Yilong Chen on 9/14/24. +// + +import SwiftUI + +struct ContentView: View { + + @EnvironmentObject var store: Store + @State private var searchText = "" + struct Props { + let movies: [Movie] + let onSearch: (String) -> Void + } + + func map(state: AppState, dispatch: @escaping Dispatcher) -> Props { + Props(movies: state.moviesState.movies, onSearch: { search in + dispatch(FetchMoviesAction(search: search)) + }) + } + + var body: some View { + + let props = map(state: store.state, dispatch: store.dispatch) + + VStack { + TextField("search", text: $searchText, onEditingChanged: { _ in }, onCommit: { + props.onSearch(searchText) + }).textFieldStyle(RoundedBorderTextFieldStyle()).padding() + + List(props.movies, id: \.imdbId) { movie in + NavigationLink(destination: MovieDetailsView(movie: movie), label: { + MovieCell(movie: movie) + }) + }.listStyle(PlainListStyle()) + } + .navigationTitle("Movies") + .embedInNavigationView() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + + let store = Store(reducer: appReducer, state: AppState(), + middlewares: [moviesMiddleware()]) + return ContentView().environmentObject(store) + } +} + +struct MovieCell: View { + let movie: Movie + var body: some View { + HStack { + URLImage(url: movie.poster) + .frame(width: 100, height: 100) + .cornerRadius(10) + Text(movie.title) + } + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/MovieDetailsView.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/MovieDetailsView.swift new file mode 100644 index 0000000..dfd788c --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/MovieDetailsView.swift @@ -0,0 +1,71 @@ +// +// MovieDetailsView.swift +// ReduxAsync +// +// Created by chenyilong on 2024/7/21. +// + +import SwiftUI + +struct MovieDetailsView: View { + @EnvironmentObject var store: Store + + let movie: Movie + struct Props { + let movieDetail: MovieDetail? + let onLoadMovieDetail: (String) -> Void + } + + private func map(state: MoviesState, dispatch: @escaping Dispatcher) -> Props { + Props(movieDetail: state.selectedMovieDetail, onLoadMovieDetail: { imdbId in + dispatch(FetchMovieDetailAction(imdbId: imdbId)) + }) + } + + var body: some View { + VStack { + let props = map(state: store.state.moviesState, dispatch: store.dispatch) + + Group { + if let movieDetail = props.movieDetail { + VStack{ + HStack { + Spacer() + URLImage(url: movieDetail.poster) + Spacer() + } + Text(movieDetail.title).padding().font(.title) + Text(movieDetail.plot).padding() + HStack { + Text("Rating") + RatingView(rating: .constant(movieDetail.imdbRating.toInt())) + Spacer() + } + } + + + } else { + Button("Load Movie Detail") { + props.onLoadMovieDetail(movie.imdbId) + } + } + } + .onAppear(perform: { + props.onLoadMovieDetail(movie.imdbId) + }) + } + + } +} + +struct MovieDetailsView_Previews: PreviewProvider { + static var previews: some View { + + let store = Store(reducer: appReducer, state: AppState(), + middlewares: [moviesMiddleware()]) + + MovieDetailsView(movie: + Movie(title: "Batman: The Killing Joke", imdbId: "tt4853102", poster: "https://m.media-amazon.com/images/M/MV5BMTdjZTliODYtNWExMi00NjQ1LWIzN2MtN2Q5NTg5NTk3NzliL2ltYWdlXkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg") + ).environmentObject(store) + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/RatingView.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/RatingView.swift new file mode 100644 index 0000000..ef6ac5c --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/RatingView.swift @@ -0,0 +1,39 @@ +// +// RatingView.swift +// ReduxAsync +// +// Created by chenyilong on 2024/7/21. +// + +import Foundation +import SwiftUI + +struct RatingView: View { + + @Binding var rating: Int? + private func starType(for index: Int) -> String { + if let rating = rating { + return index < rating ? "star.fill" : "star" + } else { + return "star" + } + } + + var body: some View { + HStack { + ForEach(0...10, id: \.self) { index in + Image(systemName: starType(for: index)) + .foregroundColor(.orange) + .onTapGesture { + self.rating = index + } + } + } + } +} + +struct RatingView_Previews: PreviewProvider { + static var previews: some View { + RatingView(rating: .constant(3)) + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/URLImage.swift b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/URLImage.swift new file mode 100755 index 0000000..2e78371 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/Shared/Views/URLImage.swift @@ -0,0 +1,39 @@ +// +// URLImage.swift +// URLImageDemo +// +// Created by Yilong Chen on 6/17/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import SwiftUI + +struct URLImage: View { + + let url: String + let placeholder: String + + @ObservedObject private var imageLoader = ImageLoader() + + init(url: String, placeholder: String = "placeholder") { + self.url = url + self.placeholder = placeholder + self.imageLoader.downloadImage(url: self.url) + } + + var body: some View { + + if let data = self.imageLoader.downloadedData { + return Image(uiImage: UIImage(data: data)!).resizable() + } else { + return Image("placeholder").resizable() + } + + } +} + +struct URLImage_Previews: PreviewProvider { + static var previews: some View { + URLImage(url: "https://fyrafix.files.wordpress.com/2011/08/url-8.jpg") + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp/iOS/Info.plist b/SwiftUI/SwiftUIRedux_MovieApp/iOS/Info.plist new file mode 100755 index 0000000..c02a6ac --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/iOS/Info.plist @@ -0,0 +1,55 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SwiftUI/SwiftUIRedux_MovieApp/macOS/Info.plist b/SwiftUI/SwiftUIRedux_MovieApp/macOS/Info.plist new file mode 100755 index 0000000..bacbc56 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/macOS/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + + diff --git a/SwiftUI/SwiftUIRedux_MovieApp/macOS/macOS.entitlements b/SwiftUI/SwiftUIRedux_MovieApp/macOS/macOS.entitlements new file mode 100755 index 0000000..f2ef3ae --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp/macOS/macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux.xcodeproj/project.pbxproj b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux.xcodeproj/project.pbxproj new file mode 100755 index 0000000..1a1e6ab --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux.xcodeproj/project.pbxproj @@ -0,0 +1,395 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 8D6FB8F625096924006518A8 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6FB8F525096924006518A8 /* Store.swift */; }; + 8DBD5C542508117B001DCB80 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DBD5C532508117B001DCB80 /* AppDelegate.swift */; }; + 8DBD5C562508117B001DCB80 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DBD5C552508117B001DCB80 /* SceneDelegate.swift */; }; + 8DBD5C582508117B001DCB80 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DBD5C572508117B001DCB80 /* ContentView.swift */; }; + 8DBD5C5A2508117F001DCB80 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DBD5C592508117F001DCB80 /* Assets.xcassets */; }; + 8DBD5C5D25081180001DCB80 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DBD5C5C25081180001DCB80 /* Preview Assets.xcassets */; }; + 8DBD5C6025081180001DCB80 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8DBD5C5E25081180001DCB80 /* LaunchScreen.storyboard */; }; + 8DC67510250A8E8C0016B26A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC6750F250A8E8C0016B26A /* Constants.swift */; }; + 8DE179502509B1C2003B9AA4 /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE1794F2509B1C2003B9AA4 /* Movie.swift */; }; + 8DE179532509B318003B9AA4 /* MovieService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE179522509B318003B9AA4 /* MovieService.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8D6FB8F525096924006518A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 8DBD5C502508117B001DCB80 /* HelloRedux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloRedux.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DBD5C532508117B001DCB80 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 8DBD5C552508117B001DCB80 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 8DBD5C572508117B001DCB80 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8DBD5C592508117F001DCB80 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8DBD5C5C25081180001DCB80 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8DBD5C5F25081180001DCB80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 8DBD5C6125081180001DCB80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8DC6750F250A8E8C0016B26A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 8DE1794F2509B1C2003B9AA4 /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; + 8DE179522509B318003B9AA4 /* MovieService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieService.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8DBD5C4D2508117B001DCB80 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8DBD5C472508117B001DCB80 = { + isa = PBXGroup; + children = ( + 8DBD5C522508117B001DCB80 /* HelloRedux */, + 8DBD5C512508117B001DCB80 /* Products */, + ); + sourceTree = ""; + }; + 8DBD5C512508117B001DCB80 /* Products */ = { + isa = PBXGroup; + children = ( + 8DBD5C502508117B001DCB80 /* HelloRedux.app */, + ); + name = Products; + sourceTree = ""; + }; + 8DBD5C522508117B001DCB80 /* HelloRedux */ = { + isa = PBXGroup; + children = ( + 8DC6750E250A8E800016B26A /* Utils */, + 8DE179512509B30A003B9AA4 /* Services */, + 8DE1794E2509B1B4003B9AA4 /* Models */, + 8DBD5C672508534C001DCB80 /* Store */, + 8DBD5C532508117B001DCB80 /* AppDelegate.swift */, + 8DBD5C552508117B001DCB80 /* SceneDelegate.swift */, + 8DBD5C572508117B001DCB80 /* ContentView.swift */, + 8DBD5C592508117F001DCB80 /* Assets.xcassets */, + 8DBD5C5E25081180001DCB80 /* LaunchScreen.storyboard */, + 8DBD5C6125081180001DCB80 /* Info.plist */, + 8DBD5C5B25081180001DCB80 /* Preview Content */, + ); + path = HelloRedux; + sourceTree = ""; + }; + 8DBD5C5B25081180001DCB80 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8DBD5C5C25081180001DCB80 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8DBD5C672508534C001DCB80 /* Store */ = { + isa = PBXGroup; + children = ( + 8D6FB8F525096924006518A8 /* Store.swift */, + ); + path = Store; + sourceTree = ""; + }; + 8DC6750E250A8E800016B26A /* Utils */ = { + isa = PBXGroup; + children = ( + 8DC6750F250A8E8C0016B26A /* Constants.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 8DE1794E2509B1B4003B9AA4 /* Models */ = { + isa = PBXGroup; + children = ( + 8DE1794F2509B1C2003B9AA4 /* Movie.swift */, + ); + path = Models; + sourceTree = ""; + }; + 8DE179512509B30A003B9AA4 /* Services */ = { + isa = PBXGroup; + children = ( + 8DE179522509B318003B9AA4 /* MovieService.swift */, + ); + path = Services; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8DBD5C4F2508117B001DCB80 /* HelloRedux */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8DBD5C6425081180001DCB80 /* Build configuration list for PBXNativeTarget "HelloRedux" */; + buildPhases = ( + 8DBD5C4C2508117B001DCB80 /* Sources */, + 8DBD5C4D2508117B001DCB80 /* Frameworks */, + 8DBD5C4E2508117B001DCB80 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HelloRedux; + productName = HelloRedux; + productReference = 8DBD5C502508117B001DCB80 /* HelloRedux.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8DBD5C482508117B001DCB80 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1150; + LastUpgradeCheck = 1150; + ORGANIZATIONNAME = "Mohammad Azam"; + TargetAttributes = { + 8DBD5C4F2508117B001DCB80 = { + CreatedOnToolsVersion = 11.5; + }; + }; + }; + buildConfigurationList = 8DBD5C4B2508117B001DCB80 /* Build configuration list for PBXProject "HelloRedux" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8DBD5C472508117B001DCB80; + productRefGroup = 8DBD5C512508117B001DCB80 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DBD5C4F2508117B001DCB80 /* HelloRedux */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8DBD5C4E2508117B001DCB80 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DBD5C6025081180001DCB80 /* LaunchScreen.storyboard in Resources */, + 8DBD5C5D25081180001DCB80 /* Preview Assets.xcassets in Resources */, + 8DBD5C5A2508117F001DCB80 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8DBD5C4C2508117B001DCB80 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DE179532509B318003B9AA4 /* MovieService.swift in Sources */, + 8DBD5C542508117B001DCB80 /* AppDelegate.swift in Sources */, + 8DBD5C562508117B001DCB80 /* SceneDelegate.swift in Sources */, + 8DBD5C582508117B001DCB80 /* ContentView.swift in Sources */, + 8DC67510250A8E8C0016B26A /* Constants.swift in Sources */, + 8D6FB8F625096924006518A8 /* Store.swift in Sources */, + 8DE179502509B1C2003B9AA4 /* Movie.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 8DBD5C5E25081180001DCB80 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 8DBD5C5F25081180001DCB80 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 8DBD5C6225081180001DCB80 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8DBD5C6325081180001DCB80 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8DBD5C6525081180001DCB80 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"HelloRedux/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = HelloRedux/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloRedux; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8DBD5C6625081180001DCB80 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"HelloRedux/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = HelloRedux/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloRedux; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8DBD5C4B2508117B001DCB80 /* Build configuration list for PBXProject "HelloRedux" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DBD5C6225081180001DCB80 /* Debug */, + 8DBD5C6325081180001DCB80 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8DBD5C6425081180001DCB80 /* Build configuration list for PBXNativeTarget "HelloRedux" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DBD5C6525081180001DCB80 /* Debug */, + 8DBD5C6625081180001DCB80 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8DBD5C482508117B001DCB80 /* Project object */; +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/AppDelegate.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/AppDelegate.swift new file mode 100755 index 0000000..5a6d6ba --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// HelloRedux +// +// Created by Yilong Chen on 9/8/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..9221b9b --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Base.lproj/LaunchScreen.storyboard b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 0000000..865e932 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/ContentView.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/ContentView.swift new file mode 100755 index 0000000..f283390 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/ContentView.swift @@ -0,0 +1,33 @@ +// +// ContentView.swift +// HelloRedux +// +// Created by Yilong Chen on 9/8/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import SwiftUI + +struct ContentView: View { + + @EnvironmentObject var store: Store + + var body: some View { + VStack { + + List(store.state.movies, id: \.imdbId) { movie in + Text(movie.title) + } + + Button("Get Movies") { + self.store.dispatch(action: GetMoviesAction()) + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView().environmentObject(store) + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Info.plist b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Info.plist new file mode 100755 index 0000000..889828a --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Info.plist @@ -0,0 +1,65 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Models/Movie.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Models/Movie.swift new file mode 100755 index 0000000..91d1126 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Models/Movie.swift @@ -0,0 +1,31 @@ +// +// Movie.swift +// HelloRedux +// +// Created by Yilong Chen on 9/9/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import Foundation + +struct MovieResponse: Decodable { + + let movies: [Movie] + + private enum CodingKeys: String, CodingKey { + case movies = "Search" + } + +} + +struct Movie: Decodable { + + let title: String + let imdbId: String + + private enum CodingKeys: String, CodingKey { + case title = "Title" + case imdbId = "imdbID" + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Preview Content/Preview Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/SceneDelegate.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/SceneDelegate.swift new file mode 100755 index 0000000..2abb8a9 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/SceneDelegate.swift @@ -0,0 +1,66 @@ +// +// SceneDelegate.swift +// HelloRedux +// +// Created by Yilong Chen on 9/8/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import UIKit +import SwiftUI + +let store = Store(reducer: reducer) + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + + // Create the SwiftUI view that provides the window contents. + let contentView = ContentView() + + // Use a UIHostingController as window root view controller. + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: contentView.environmentObject(store)) + self.window = window + window.makeKeyAndVisible() + } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Services/MovieService.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Services/MovieService.swift new file mode 100755 index 0000000..c935304 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Services/MovieService.swift @@ -0,0 +1,44 @@ +// +// MovieService.swift +// HelloRedux +// +// Created by Yilong Chen on 9/9/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import Foundation + +enum NetworkError: Error { + case invalidURL + case decodingError + case noData +} + +class MovieService { + + func getMovies(url: String, completion: @escaping (Result<[Movie],NetworkError>) -> Void) { + + guard let moviesURL = URL(string: url) else { + completion(.failure(.invalidURL)) + return + } + + URLSession.shared.dataTask(with: moviesURL) { data, response, error in + + guard let data = data, error == nil else { + completion(.failure(.noData)) + return + } + + let movieResponse = try? JSONDecoder().decode(MovieResponse.self, from: data) + if let movieResponse = movieResponse { + completion(.success(movieResponse.movies)) + } else { + completion(.failure(.decodingError)) + } + + }.resume() + } + +} + diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Store/Store.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Store/Store.swift new file mode 100755 index 0000000..7f60bea --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Store/Store.swift @@ -0,0 +1,80 @@ +// +// Store.swift +// HelloRedux +// +// Created by Yilong Chen on 9/9/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import Foundation + +typealias Reducer = (State, Action) -> State + +struct State { + var counter = 0 + var movies = [Movie]() +} + +protocol Action { } + +struct IncrementAction: Action { } + +struct GetMoviesAction: Action { + + init() { + + MovieService().getMovies(url: Constants.MOVIES_URL) { result in + switch result { + case .success(let movies): + store.dispatch(action: SetMoviesAction(movies: movies)) + case .failure(let error): + print(error.localizedDescription) + } + } + + } +} + +struct SetMoviesAction: Action { + let movies: [Movie] + + init(movies: [Movie]) { + self.movies = movies + } +} + + + +func reducer(state: State, action: Action) -> State { + var state = state + + switch action { + case _ as IncrementAction: + state.counter += 1 + case let action as SetMoviesAction: + state.movies = action.movies + + default: + break + } + + return state +} + +class Store: ObservableObject { + + var reducer: Reducer + @Published private (set) var state: State + + init(reducer: @escaping Reducer, state: State = State()) { + self.reducer = reducer + self.state = state + } + + func dispatch(action: Action) { + DispatchQueue.main.async { + self.state = self.reducer(self.state, action) + } + } + +} diff --git a/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Utils/Constants.swift b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Utils/Constants.swift new file mode 100755 index 0000000..26090a9 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_MovieApp_101/HelloRedux/Utils/Constants.swift @@ -0,0 +1,15 @@ +// +// Constants.swift +// HelloRedux +// +// Created by Yilong Chen on 9/10/24. +// Copyright © 2024 Yilong Chen. All rights reserved. +// + +import Foundation + +struct Constants { + + static let MOVIES_URL = "http://www.omdbapi.com/?s=batman&apikey=YOUAPIKEY" + +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder.xcodeproj/project.pbxproj b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder.xcodeproj/project.pbxproj new file mode 100755 index 0000000..3668357 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder.xcodeproj/project.pbxproj @@ -0,0 +1,767 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 76E1C79A2C155EE6006A98A5 /* RestRoomFinderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7992C155EE6006A98A5 /* RestRoomFinderApp.swift */; }; + 76E1C79E2C155EE8006A98A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76E1C79D2C155EE8006A98A5 /* Assets.xcassets */; }; + 76E1C7A12C155EE8006A98A5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76E1C7A02C155EE8006A98A5 /* Preview Assets.xcassets */; }; + 76E1C7AB2C155EE8006A98A5 /* RestRoomFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7AA2C155EE8006A98A5 /* RestRoomFinderTests.swift */; }; + 76E1C7B52C155EE8006A98A5 /* RestRoomFinderUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7B42C155EE8006A98A5 /* RestRoomFinderUITests.swift */; }; + 76E1C7B72C155EE8006A98A5 /* RestRoomFinderUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7B62C155EE8006A98A5 /* RestRoomFinderUITestsLaunchTests.swift */; }; + 76E1C7D92C156353006A98A5 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7C32C156352006A98A5 /* String+Extensions.swift */; }; + 76E1C7DA2C156353006A98A5 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7C42C156352006A98A5 /* View+Extensions.swift */; }; + 76E1C7DC2C156353006A98A5 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7C82C156352006A98A5 /* HomeScreen.swift */; }; + 76E1C7DD2C156353006A98A5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7CA2C156352006A98A5 /* Constants.swift */; }; + 76E1C7E02C156353006A98A5 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7D02C156353006A98A5 /* Actions.swift */; }; + 76E1C7E22C156353006A98A5 /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7D42C156353006A98A5 /* AppReducer.swift */; }; + 76E1C7E42C156353006A98A5 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7D72C156353006A98A5 /* Store.swift */; }; + 76E1C7E72C15671E006A98A5 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7E62C15671E006A98A5 /* NetworkManager.swift */; }; + 76E1C7EA2C156792006A98A5 /* MainNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7E92C156792006A98A5 /* MainNetworkManager.swift */; }; + 76E1C7EC2C156867006A98A5 /* Restroom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7EB2C156867006A98A5 /* Restroom.swift */; }; + 76E1C7EE2C156968006A98A5 /* RestroomService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7ED2C156968006A98A5 /* RestroomService.swift */; }; + 76E1C7F12C1569EA006A98A5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7F02C1569EA006A98A5 /* Logger.swift */; }; + 76E1C7F32C156C24006A98A5 /* RestroomReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7F22C156C24006A98A5 /* RestroomReducer.swift */; }; + 76E1C7F52C156D7D006A98A5 /* RestroomsMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7F42C156D7D006A98A5 /* RestroomsMiddleware.swift */; }; + 76E1C7F82C156F9A006A98A5 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7F72C156F9A006A98A5 /* LocationManager.swift */; }; + 76E1C7FE2C157630006A98A5 /* RestroomListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7FD2C157630006A98A5 /* RestroomListRow.swift */; }; + 76E1C8002C1694E2006A98A5 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E1C7FF2C1694E2006A98A5 /* ConsoleLogger.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 76E1C7A72C155EE8006A98A5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 76E1C78E2C155EE6006A98A5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 76E1C7952C155EE6006A98A5; + remoteInfo = RestRoomFinder; + }; + 76E1C7B12C155EE8006A98A5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 76E1C78E2C155EE6006A98A5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 76E1C7952C155EE6006A98A5; + remoteInfo = RestRoomFinder; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 76E1C7962C155EE6006A98A5 /* RestRoomFinder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RestRoomFinder.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 76E1C7992C155EE6006A98A5 /* RestRoomFinderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestRoomFinderApp.swift; sourceTree = ""; }; + 76E1C79D2C155EE8006A98A5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 76E1C7A02C155EE8006A98A5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 76E1C7A62C155EE8006A98A5 /* RestRoomFinderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RestRoomFinderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 76E1C7AA2C155EE8006A98A5 /* RestRoomFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestRoomFinderTests.swift; sourceTree = ""; }; + 76E1C7B02C155EE8006A98A5 /* RestRoomFinderUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RestRoomFinderUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 76E1C7B42C155EE8006A98A5 /* RestRoomFinderUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestRoomFinderUITests.swift; sourceTree = ""; }; + 76E1C7B62C155EE8006A98A5 /* RestRoomFinderUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestRoomFinderUITestsLaunchTests.swift; sourceTree = ""; }; + 76E1C7C32C156352006A98A5 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 76E1C7C42C156352006A98A5 /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; + 76E1C7C82C156352006A98A5 /* HomeScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; + 76E1C7CA2C156352006A98A5 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 76E1C7D02C156353006A98A5 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; + 76E1C7D42C156353006A98A5 /* AppReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; + 76E1C7D72C156353006A98A5 /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 76E1C7E52C1565B5006A98A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 76E1C7E62C15671E006A98A5 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + 76E1C7E92C156792006A98A5 /* MainNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNetworkManager.swift; sourceTree = ""; }; + 76E1C7EB2C156867006A98A5 /* Restroom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Restroom.swift; sourceTree = ""; }; + 76E1C7ED2C156968006A98A5 /* RestroomService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestroomService.swift; sourceTree = ""; }; + 76E1C7F02C1569EA006A98A5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 76E1C7F22C156C24006A98A5 /* RestroomReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestroomReducer.swift; sourceTree = ""; }; + 76E1C7F42C156D7D006A98A5 /* RestroomsMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestroomsMiddleware.swift; sourceTree = ""; }; + 76E1C7F72C156F9A006A98A5 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; + 76E1C7FD2C157630006A98A5 /* RestroomListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestroomListRow.swift; sourceTree = ""; }; + 76E1C7FF2C1694E2006A98A5 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 76E1C7932C155EE6006A98A5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 76E1C7A32C155EE8006A98A5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 76E1C7AD2C155EE8006A98A5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 76E1C78D2C155EE6006A98A5 = { + isa = PBXGroup; + children = ( + 76E1C7982C155EE6006A98A5 /* RestRoomFinder */, + 76E1C7A92C155EE8006A98A5 /* RestRoomFinderTests */, + 76E1C7B32C155EE8006A98A5 /* RestRoomFinderUITests */, + 76E1C7972C155EE6006A98A5 /* Products */, + ); + sourceTree = ""; + }; + 76E1C7972C155EE6006A98A5 /* Products */ = { + isa = PBXGroup; + children = ( + 76E1C7962C155EE6006A98A5 /* RestRoomFinder.app */, + 76E1C7A62C155EE8006A98A5 /* RestRoomFinderTests.xctest */, + 76E1C7B02C155EE8006A98A5 /* RestRoomFinderUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 76E1C7982C155EE6006A98A5 /* RestRoomFinder */ = { + isa = PBXGroup; + children = ( + 76E1C7E52C1565B5006A98A5 /* Info.plist */, + 76E1C7C52C156352006A98A5 /* Extensions */, + 76E1C7C72C156352006A98A5 /* Managers */, + 76E1C7CF2C156353006A98A5 /* Models */, + 76E1C7CD2C156353006A98A5 /* Services */, + 76E1C7D82C156353006A98A5 /* Store */, + 76E1C7CB2C156352006A98A5 /* Utils */, + 76E1C7C92C156352006A98A5 /* Views */, + 76E1C7992C155EE6006A98A5 /* RestRoomFinderApp.swift */, + 76E1C79D2C155EE8006A98A5 /* Assets.xcassets */, + 76E1C79F2C155EE8006A98A5 /* Preview Content */, + ); + path = RestRoomFinder; + sourceTree = ""; + }; + 76E1C79F2C155EE8006A98A5 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 76E1C7A02C155EE8006A98A5 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 76E1C7A92C155EE8006A98A5 /* RestRoomFinderTests */ = { + isa = PBXGroup; + children = ( + 76E1C7AA2C155EE8006A98A5 /* RestRoomFinderTests.swift */, + ); + path = RestRoomFinderTests; + sourceTree = ""; + }; + 76E1C7B32C155EE8006A98A5 /* RestRoomFinderUITests */ = { + isa = PBXGroup; + children = ( + 76E1C7B42C155EE8006A98A5 /* RestRoomFinderUITests.swift */, + 76E1C7B62C155EE8006A98A5 /* RestRoomFinderUITestsLaunchTests.swift */, + ); + path = RestRoomFinderUITests; + sourceTree = ""; + }; + 76E1C7C52C156352006A98A5 /* Extensions */ = { + isa = PBXGroup; + children = ( + 76E1C7C32C156352006A98A5 /* String+Extensions.swift */, + 76E1C7C42C156352006A98A5 /* View+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 76E1C7C72C156352006A98A5 /* Managers */ = { + isa = PBXGroup; + children = ( + 76E1C7F62C156F92006A98A5 /* LocationManager */, + 76E1C7EF2C1569E4006A98A5 /* Logger */, + 76E1C7E82C156722006A98A5 /* Network Manager */, + ); + path = Managers; + sourceTree = ""; + }; + 76E1C7C92C156352006A98A5 /* Views */ = { + isa = PBXGroup; + children = ( + 76E1C7C82C156352006A98A5 /* HomeScreen.swift */, + 76E1C7FD2C157630006A98A5 /* RestroomListRow.swift */, + ); + path = Views; + sourceTree = ""; + }; + 76E1C7CB2C156352006A98A5 /* Utils */ = { + isa = PBXGroup; + children = ( + 76E1C7CA2C156352006A98A5 /* Constants.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 76E1C7CD2C156353006A98A5 /* Services */ = { + isa = PBXGroup; + children = ( + 76E1C7ED2C156968006A98A5 /* RestroomService.swift */, + ); + path = Services; + sourceTree = ""; + }; + 76E1C7CF2C156353006A98A5 /* Models */ = { + isa = PBXGroup; + children = ( + 76E1C7EB2C156867006A98A5 /* Restroom.swift */, + ); + path = Models; + sourceTree = ""; + }; + 76E1C7D12C156353006A98A5 /* Actions */ = { + isa = PBXGroup; + children = ( + 76E1C7D02C156353006A98A5 /* Actions.swift */, + ); + path = Actions; + sourceTree = ""; + }; + 76E1C7D32C156353006A98A5 /* Middlewares */ = { + isa = PBXGroup; + children = ( + 76E1C7F42C156D7D006A98A5 /* RestroomsMiddleware.swift */, + ); + path = Middlewares; + sourceTree = ""; + }; + 76E1C7D62C156353006A98A5 /* Reducers */ = { + isa = PBXGroup; + children = ( + 76E1C7D42C156353006A98A5 /* AppReducer.swift */, + 76E1C7F22C156C24006A98A5 /* RestroomReducer.swift */, + ); + path = Reducers; + sourceTree = ""; + }; + 76E1C7D82C156353006A98A5 /* Store */ = { + isa = PBXGroup; + children = ( + 76E1C7D12C156353006A98A5 /* Actions */, + 76E1C7D32C156353006A98A5 /* Middlewares */, + 76E1C7D62C156353006A98A5 /* Reducers */, + 76E1C7D72C156353006A98A5 /* Store.swift */, + ); + path = Store; + sourceTree = ""; + }; + 76E1C7E82C156722006A98A5 /* Network Manager */ = { + isa = PBXGroup; + children = ( + 76E1C7E62C15671E006A98A5 /* NetworkManager.swift */, + 76E1C7E92C156792006A98A5 /* MainNetworkManager.swift */, + ); + path = "Network Manager"; + sourceTree = ""; + }; + 76E1C7EF2C1569E4006A98A5 /* Logger */ = { + isa = PBXGroup; + children = ( + 76E1C7F02C1569EA006A98A5 /* Logger.swift */, + 76E1C7FF2C1694E2006A98A5 /* ConsoleLogger.swift */, + ); + path = Logger; + sourceTree = ""; + }; + 76E1C7F62C156F92006A98A5 /* LocationManager */ = { + isa = PBXGroup; + children = ( + 76E1C7F72C156F9A006A98A5 /* LocationManager.swift */, + ); + path = LocationManager; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 76E1C7952C155EE6006A98A5 /* RestRoomFinder */ = { + isa = PBXNativeTarget; + buildConfigurationList = 76E1C7BA2C155EE8006A98A5 /* Build configuration list for PBXNativeTarget "RestRoomFinder" */; + buildPhases = ( + 76E1C7922C155EE6006A98A5 /* Sources */, + 76E1C7932C155EE6006A98A5 /* Frameworks */, + 76E1C7942C155EE6006A98A5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RestRoomFinder; + productName = RestRoomFinder; + productReference = 76E1C7962C155EE6006A98A5 /* RestRoomFinder.app */; + productType = "com.apple.product-type.application"; + }; + 76E1C7A52C155EE8006A98A5 /* RestRoomFinderTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 76E1C7BD2C155EE8006A98A5 /* Build configuration list for PBXNativeTarget "RestRoomFinderTests" */; + buildPhases = ( + 76E1C7A22C155EE8006A98A5 /* Sources */, + 76E1C7A32C155EE8006A98A5 /* Frameworks */, + 76E1C7A42C155EE8006A98A5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 76E1C7A82C155EE8006A98A5 /* PBXTargetDependency */, + ); + name = RestRoomFinderTests; + productName = RestRoomFinderTests; + productReference = 76E1C7A62C155EE8006A98A5 /* RestRoomFinderTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 76E1C7AF2C155EE8006A98A5 /* RestRoomFinderUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 76E1C7C02C155EE8006A98A5 /* Build configuration list for PBXNativeTarget "RestRoomFinderUITests" */; + buildPhases = ( + 76E1C7AC2C155EE8006A98A5 /* Sources */, + 76E1C7AD2C155EE8006A98A5 /* Frameworks */, + 76E1C7AE2C155EE8006A98A5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 76E1C7B22C155EE8006A98A5 /* PBXTargetDependency */, + ); + name = RestRoomFinderUITests; + productName = RestRoomFinderUITests; + productReference = 76E1C7B02C155EE8006A98A5 /* RestRoomFinderUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 76E1C78E2C155EE6006A98A5 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 76E1C7952C155EE6006A98A5 = { + CreatedOnToolsVersion = 15.4; + }; + 76E1C7A52C155EE8006A98A5 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = 76E1C7952C155EE6006A98A5; + }; + 76E1C7AF2C155EE8006A98A5 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = 76E1C7952C155EE6006A98A5; + }; + }; + }; + buildConfigurationList = 76E1C7912C155EE6006A98A5 /* Build configuration list for PBXProject "RestRoomFinder" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 76E1C78D2C155EE6006A98A5; + productRefGroup = 76E1C7972C155EE6006A98A5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 76E1C7952C155EE6006A98A5 /* RestRoomFinder */, + 76E1C7A52C155EE8006A98A5 /* RestRoomFinderTests */, + 76E1C7AF2C155EE8006A98A5 /* RestRoomFinderUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 76E1C7942C155EE6006A98A5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 76E1C7A12C155EE8006A98A5 /* Preview Assets.xcassets in Resources */, + 76E1C79E2C155EE8006A98A5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 76E1C7A42C155EE8006A98A5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 76E1C7AE2C155EE8006A98A5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 76E1C7922C155EE6006A98A5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 76E1C7D92C156353006A98A5 /* String+Extensions.swift in Sources */, + 76E1C7E72C15671E006A98A5 /* NetworkManager.swift in Sources */, + 76E1C7EC2C156867006A98A5 /* Restroom.swift in Sources */, + 76E1C7DC2C156353006A98A5 /* HomeScreen.swift in Sources */, + 76E1C7F82C156F9A006A98A5 /* LocationManager.swift in Sources */, + 76E1C7E02C156353006A98A5 /* Actions.swift in Sources */, + 76E1C7E42C156353006A98A5 /* Store.swift in Sources */, + 76E1C7E22C156353006A98A5 /* AppReducer.swift in Sources */, + 76E1C7F52C156D7D006A98A5 /* RestroomsMiddleware.swift in Sources */, + 76E1C7FE2C157630006A98A5 /* RestroomListRow.swift in Sources */, + 76E1C7DD2C156353006A98A5 /* Constants.swift in Sources */, + 76E1C7EE2C156968006A98A5 /* RestroomService.swift in Sources */, + 76E1C7F32C156C24006A98A5 /* RestroomReducer.swift in Sources */, + 76E1C7F12C1569EA006A98A5 /* Logger.swift in Sources */, + 76E1C7EA2C156792006A98A5 /* MainNetworkManager.swift in Sources */, + 76E1C8002C1694E2006A98A5 /* ConsoleLogger.swift in Sources */, + 76E1C7DA2C156353006A98A5 /* View+Extensions.swift in Sources */, + 76E1C79A2C155EE6006A98A5 /* RestRoomFinderApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 76E1C7A22C155EE8006A98A5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 76E1C7AB2C155EE8006A98A5 /* RestRoomFinderTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 76E1C7AC2C155EE8006A98A5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 76E1C7B52C155EE8006A98A5 /* RestRoomFinderUITests.swift in Sources */, + 76E1C7B72C155EE8006A98A5 /* RestRoomFinderUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 76E1C7A82C155EE8006A98A5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 76E1C7952C155EE6006A98A5 /* RestRoomFinder */; + targetProxy = 76E1C7A72C155EE8006A98A5 /* PBXContainerItemProxy */; + }; + 76E1C7B22C155EE8006A98A5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 76E1C7952C155EE6006A98A5 /* RestRoomFinder */; + targetProxy = 76E1C7B12C155EE8006A98A5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 76E1C7B82C155EE8006A98A5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 76E1C7B92C155EE8006A98A5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 76E1C7BB2C155EE8006A98A5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"RestRoomFinder/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = RestRoomFinder/Info.plist; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Restroom Finder requires access to your location."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Restroom Finder requires access to your location."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.takasurazeem.RestRoomFinder; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 76E1C7BC2C155EE8006A98A5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"RestRoomFinder/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = RestRoomFinder/Info.plist; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Restroom Finder requires access to your location."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Restroom Finder requires access to your location."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.takasurazeem.RestRoomFinder; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 76E1C7BE2C155EE8006A98A5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.takasurazeem.RestRoomFinderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RestRoomFinder.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/RestRoomFinder"; + }; + name = Debug; + }; + 76E1C7BF2C155EE8006A98A5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.takasurazeem.RestRoomFinderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RestRoomFinder.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/RestRoomFinder"; + }; + name = Release; + }; + 76E1C7C12C155EE8006A98A5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.takasurazeem.RestRoomFinderUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RestRoomFinder; + }; + name = Debug; + }; + 76E1C7C22C155EE8006A98A5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.takasurazeem.RestRoomFinderUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RestRoomFinder; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 76E1C7912C155EE6006A98A5 /* Build configuration list for PBXProject "RestRoomFinder" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 76E1C7B82C155EE8006A98A5 /* Debug */, + 76E1C7B92C155EE8006A98A5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 76E1C7BA2C155EE8006A98A5 /* Build configuration list for PBXNativeTarget "RestRoomFinder" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 76E1C7BB2C155EE8006A98A5 /* Debug */, + 76E1C7BC2C155EE8006A98A5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 76E1C7BD2C155EE8006A98A5 /* Build configuration list for PBXNativeTarget "RestRoomFinderTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 76E1C7BE2C155EE8006A98A5 /* Debug */, + 76E1C7BF2C155EE8006A98A5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 76E1C7C02C155EE8006A98A5 /* Build configuration list for PBXNativeTarget "RestRoomFinderUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 76E1C7C12C155EE8006A98A5 /* Debug */, + 76E1C7C22C155EE8006A98A5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 76E1C78E2C155EE6006A98A5 /* Project object */; +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100755 index 0000000..eb87897 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..13613e3 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Extensions/String+Extensions.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Extensions/String+Extensions.swift new file mode 100755 index 0000000..cc9ede6 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Extensions/String+Extensions.swift @@ -0,0 +1,11 @@ + + +import Foundation + +extension String { + + func encodeURL() -> String? { + return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + } + +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Extensions/View+Extensions.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Extensions/View+Extensions.swift new file mode 100755 index 0000000..9970605 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Extensions/View+Extensions.swift @@ -0,0 +1,10 @@ + + +import Foundation +import SwiftUI + +extension View { + func embedInNavigationView() -> some View { + NavigationView { self } + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Info.plist b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Info.plist new file mode 100755 index 0000000..f7422bf --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Info.plist @@ -0,0 +1,25 @@ + + + + + + NSLocationUsageDescription + $(PRODUCT_NAME) location use + + + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) location use + + + NSLocationAlwaysUsageDescription + $(PRODUCT_NAME) always uses location + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/LocationManager/LocationManager.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/LocationManager/LocationManager.swift new file mode 100755 index 0000000..6146359 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/LocationManager/LocationManager.swift @@ -0,0 +1,46 @@ +// +// LocationManager.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation +import CoreLocation + +class LocationManager: NSObject, ObservableObject { + private let locationManager = CLLocationManager() + @Published var location: CLLocation? = nil + + override init() { + super.init() + + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.distanceFilter = kCLHeadingFilterNone + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + } + + func updateLocation() { + locationManager.startUpdatingLocation() + } +} + +extension LocationManager: CLLocationManagerDelegate { + func locationManager( + _ manager: CLLocationManager, + didUpdateLocations locations: [CLLocation] + ) { + guard let location = locations.last else { + locationManager.stopUpdatingLocation() + return + } + + DispatchQueue.main.async { + self.location = location + } + + locationManager.stopUpdatingLocation() + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Logger/ConsoleLogger.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Logger/ConsoleLogger.swift new file mode 100755 index 0000000..4d989f3 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Logger/ConsoleLogger.swift @@ -0,0 +1,49 @@ +// +// ConsoleLogger.swift +// RestRoomFinder +// ConsoleLogger to the project that leverages Xcode and Swift's Unified Logging feature. + +// Created by Yilong Chen on 10/06/2024. +// + +import os +import Foundation + +class ConsoleLogger: Logger { + private let subsystem: String + private let category: String + private let logger: OSLog + + init( + subsystem: String = Bundle.main.bundleIdentifier ?? "default.subsystem", + category: String + ) { + self.subsystem = subsystem + self.category = category + self.logger = OSLog(subsystem: subsystem, category: category) + } + + func debug(_ message: String) { + #if DEBUG + os_log("%{public}@", log: logger, type: .debug, message) + #endif + } + + func info(_ message: String) { + #if DEBUG + os_log("%{public}@", log: logger, type: .info, message) + #endif + } + + func error(_ message: String) { + #if DEBUG + os_log("%{public}@", log: logger, type: .error, message) + #endif + } + + func fault(_ message: String) { + #if DEBUG + os_log("%{public}@", log: logger, type: .fault, message) + #endif + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Logger/Logger.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Logger/Logger.swift new file mode 100755 index 0000000..20739d0 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Logger/Logger.swift @@ -0,0 +1,15 @@ +// +// Logger.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +public protocol Logger { + func debug(_ message: String) + func info(_ message: String) + func error(_ message: String) + func fault(_ message: String) +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Network Manager/MainNetworkManager.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Network Manager/MainNetworkManager.swift new file mode 100755 index 0000000..b93ad2d --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Network Manager/MainNetworkManager.swift @@ -0,0 +1,59 @@ +// +// MainNetworkManager.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +// Main implementation of Network Manager +struct MainNetworkManager: NetworkManager { + + // Generic request function + private func request( + url: URL, + method: HTTPMethod, + body: Data? = nil + ) async throws -> T { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.httpBody = body + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { + throw NetworkError.requestFailed + } + + do { + let decodedResponse = try JSONDecoder().decode(T.self, from: data) + return decodedResponse + } catch { + throw NetworkError.decodingFailed + } + } + + // GET Request + func get( + urlString: String + ) async throws -> T { + guard let url = URL(string: urlString) else { + throw NetworkError.invalidURL + } + return try await request(url: url, method: .GET) + } + + // POST Request + func post( + urlString: String, + body: U + ) async throws -> T { + guard let url = URL(string: urlString) else { + throw NetworkError.invalidURL + } + let bodyData = try JSONEncoder().encode(body) + return try await request(url: url, method: .POST, body: bodyData) + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Network Manager/NetworkManager.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Network Manager/NetworkManager.swift new file mode 100755 index 0000000..97a5b05 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Managers/Network Manager/NetworkManager.swift @@ -0,0 +1,28 @@ +// +// NetworkManager.swift +// RestRoomFinder +// NetworkManager class that is async-await based +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +// Enum for HTTP Methods +enum HTTPMethod: String { + case GET + case POST +} + +// Enum for Network Errors +enum NetworkError: Error { + case invalidURL + case requestFailed + case invalidResponse + case decodingFailed +} + +// Protocol for Network Manager +protocol NetworkManager { + func get(urlString: String) async throws -> T + func post(urlString: String, body: U) async throws -> T +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Models/Restroom.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Models/Restroom.swift new file mode 100755 index 0000000..f672696 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Models/Restroom.swift @@ -0,0 +1,42 @@ +// +// Restroom.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +struct Restroom: Decodable, Identifiable { + let id: Int + let name: String? + let distance: Double + let street: String + let city: String + let state: String + let accessible: Bool + let unisex: Bool + let directions: String? + let comment: String? + let latitude: Double + let longitude: Double + + var addess: String { + "\(street), \(city) \(state)" + } + + static let example = Restroom( + id: 451, + name: "Finca Son Verdera Mallorca", + distance: 0.00014083525406852245, + street: "en el campo", + city: "San Francisco", + state: "CA", + accessible: true, + unisex: true, + directions: "http://www.finca-sonverdera.de", + comment: "also wlan access", + latitude: 37.7749295, + longitude: -122.4194155 + ) +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Preview Content/Preview Assets.xcassets/Contents.json b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/RestRoomFinderApp.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/RestRoomFinderApp.swift new file mode 100755 index 0000000..00caef5 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/RestRoomFinderApp.swift @@ -0,0 +1,41 @@ +// +// RestRoomFinderApp.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import SwiftUI + +@main +struct RestRoomFinderApp: App { + + init() { + configureTheme() + } + + var body: some Scene { + + let store = Store( + reducer: appReducer, + state: AppState(), + middlewares: [restroomMiddleware()] + ) + + WindowGroup { + HomeScreen() + .environmentObject(store) + } + } + + private func configureTheme() { + UINavigationBar + .appearance() + .backgroundColor = UIColor( + displayP3Red: 44/255, + green: 62/255, + blue: 80/255, + alpha: 1.0 + ) + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Services/RestroomService.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Services/RestroomService.swift new file mode 100755 index 0000000..39da3a8 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Services/RestroomService.swift @@ -0,0 +1,23 @@ +// +// RestroomService.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +struct RestroomService { + + func getRestRoomByLatLong(lat: Double, long: Double) async throws -> [Restroom]? { + let urlString = Constants.Urls.restroomsByLatAndLng(lat: lat, lng: long) + + logger.debug(urlString) + + let restrooms: [Restroom] = try await networkManager.get(urlString: urlString) + return restrooms + } + + let logger: Logger + let networkManager: NetworkManager +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Actions/Actions.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Actions/Actions.swift new file mode 100755 index 0000000..2259c1d --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Actions/Actions.swift @@ -0,0 +1,19 @@ +// +// Actions.swift +// RestRoomFinder +// +// Created by Yilong Chen on 10/15/24. +// + +import Foundation + +protocol Action { } + +struct FetchRestroomsAction: Action { + let latitude: Double + let longitude: Double +} + +struct SetRestroomsAction: Action { + let restrooms: [Restroom] +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Middlewares/RestroomsMiddleware.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Middlewares/RestroomsMiddleware.swift new file mode 100755 index 0000000..e51272a --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Middlewares/RestroomsMiddleware.swift @@ -0,0 +1,37 @@ +// +// RestroomsMiddleware.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +func restroomMiddleware() -> Middleware { + return { + state, + action, + dispatch in + switch action { + case let action as FetchRestroomsAction: + fetchRestrooms(action: action, dispatch: dispatch) + default: + break + } + } +} + +private func fetchRestrooms(action: FetchRestroomsAction, dispatch: @escaping Dispatcher) { + let logger = ConsoleLogger(category: "restRoomMiddleware") + Task { + do { + let restrooms = try await RestroomService( + logger: logger, + networkManager: MainNetworkManager() + ).getRestRoomByLatLong(lat: action.latitude, long: action.longitude) + dispatch(SetRestroomsAction(restrooms: restrooms ?? [])) + } catch { + logger.error(error.localizedDescription) + } + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Reducers/AppReducer.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Reducers/AppReducer.swift new file mode 100755 index 0000000..fcd9fe2 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Reducers/AppReducer.swift @@ -0,0 +1,18 @@ +// +// AppReducer.swift +// CombiningReducers +// +// Created by Yilong Chen on 9/15/24. +// + +import Foundation + +func appReducer( + _ state: AppState, + _ action: Action +) -> AppState { + + var state = state + state.restrooms = restroomsReducer(state.restrooms, action) + return state +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Reducers/RestroomReducer.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Reducers/RestroomReducer.swift new file mode 100755 index 0000000..5419f9d --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Reducers/RestroomReducer.swift @@ -0,0 +1,22 @@ +// +// RestroomReducer.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import Foundation + +func restroomsReducer( + _ state: RestRoomState, + _ action: Action +) -> RestRoomState { + var state = state + switch action { + case let action as SetRestroomsAction: + state.restrooms = action.restrooms + default: + break + } + return state +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Store.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Store.swift new file mode 100755 index 0000000..5852f9d --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Store/Store.swift @@ -0,0 +1,49 @@ +// +// Store.swift +// HelloRedux +// +// Created by Yilong Chen on 9/14/24. +// + +import Foundation + +typealias Dispatcher = (Action) -> Void + +typealias Reducer = (_ state: State, _ action: Action) -> State +typealias Middleware = (StoreState, Action, @escaping Dispatcher) -> Void + +protocol ReduxState { } + +struct AppState: ReduxState { + var restrooms: RestRoomState = RestRoomState() +} + +struct RestRoomState: ReduxState { + var restrooms: [Restroom] = [] +} + +class Store: ObservableObject { + + var reducer: Reducer + @Published var state: StoreState + var middlewares: [Middleware] + + init(reducer: @escaping Reducer, state: StoreState, + middlewares: [Middleware] = []) { + self.reducer = reducer + self.state = state + self.middlewares = middlewares + } + + func dispatch(action: Action) { + DispatchQueue.main.async { + self.state = self.reducer(self.state, action) + } + + // run all middlewares + middlewares.forEach { middleware in + middleware(state, action, dispatch) + } + } + +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Utils/Constants.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Utils/Constants.swift new file mode 100755 index 0000000..bb893ae --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Utils/Constants.swift @@ -0,0 +1,11 @@ + + +import Foundation + +struct Constants { + struct Urls { + static func restroomsByLatAndLng(lat: Double, lng: Double) -> String { + "https://www.refugerestrooms.org/api/v1/restrooms/by_location?lat=\(lat)&lng=\(lng)" + } + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Views/HomeScreen.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Views/HomeScreen.swift new file mode 100755 index 0000000..76347c4 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Views/HomeScreen.swift @@ -0,0 +1,82 @@ +// +// Actions.swift +// RestRoomFinder +// +// Created by Yilong Chen on 10/15/24. +// + +import SwiftUI +import Combine + +struct HomeScreen: View { + + @EnvironmentObject var store: Store + + @ObservedObject private var locationManager = LocationManager() + @State private var cancellables: AnyCancellable? = nil + + struct Props { + let restrooms: [Restroom] + let onFetchRestroomsByLatLong: (Double, Double) -> Void + } + + private func map(state: RestRoomState) -> Props { + Props(restrooms: state.restrooms) { lat, long in + store.dispatch(action: FetchRestroomsAction(latitude: lat, longitude: long)) + } + } + + var body: some View { + let props = map(state: store.state.restrooms) + VStack(alignment: .leading) { + HStack { + EmptyView() + } + .frame(maxWidth: .infinity, maxHeight: 44) + Spacer() + HStack { + Text("Restrooms") + .foregroundColor(.white) + .font(.largeTitle) + Spacer() + Button { + locationManager.updateLocation() + } label: { + Image(systemName: "arrow.clockwise.circle") + .font(.title) + .foregroundColor(.white) + } + } + .padding() + List(props.restrooms) { restroom in + RestroomListRow(restroom: restroom) + } + .buttonStyle(PlainButtonStyle()) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(#colorLiteral(red: 0.880972445, green: 0.3729454875, blue: 0.2552506924, alpha: 1))) + .edgesIgnoringSafeArea(.all) + .onAppear { + self.cancellables = locationManager.$location.sink { location in + if let location { + props.onFetchRestroomsByLatLong( + location.coordinate.latitude, + location.coordinate.longitude + ) + } + } + } + } +} + +struct HomeScreen_Previews: PreviewProvider { + static var previews: some View { + let store = Store( + reducer: appReducer, + state: AppState(), + middlewares: [restroomMiddleware()] + ) + HomeScreen() + .environmentObject(store) + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Views/RestroomListRow.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Views/RestroomListRow.swift new file mode 100755 index 0000000..bbe8302 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinder/Views/RestroomListRow.swift @@ -0,0 +1,60 @@ +// +// RestroomListRow.swift +// RestRoomFinder +// +// Created by Yilong Chen on 09/06/2024. +// + +import SwiftUI + +struct RestroomListRow: View { + let restroom: Restroom + // Should have been injected, infact, should be in the viewModel? + let logger = ConsoleLogger(category: "URL") + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text(restroom.name ?? "Name not available") + .font(.headline) + Spacer() + Text(String(format: "%.2f miles", restroom.distance)) + } + .padding(.top, 10) + Text(restroom.addess) + .font(.subheadline) + .opacity(0.5) + Button("Directions") { + guard let targetURL = URL(string: "https://maps.apple.com/?address=\(restroom.addess.encodeURL() ?? "")") else { + logger.debug("https://maps.apple.com/?address=\(restroom.addess.encodeURL() ?? "")") + return + } + logger.debug(targetURL.absoluteString) + if UIApplication.shared.canOpenURL(targetURL) { + UIApplication.shared.open(targetURL) + } else { + logger.error("Could not open URL") + } + } + .font(.caption) + .foregroundColor(.white) + .padding(6) + .background(Color(#colorLiteral(red: 0.184266597, green: 0.8003296256, blue: 0.4435204864, alpha: 1))) + .cornerRadius(6) + + Text(restroom.comment ?? "") + .font(.footnote) + + HStack { + Text(restroom.accessible ? "♿️" : "") + } + } + } +} + +struct RestroomListRow_Previews: PreviewProvider { + static var previews: some View { + RestroomListRow( + restroom: .example + ) + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderTests/RestRoomFinderTests.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderTests/RestRoomFinderTests.swift new file mode 100755 index 0000000..cefef23 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderTests/RestRoomFinderTests.swift @@ -0,0 +1,36 @@ +// +// RestRoomFinderTests.swift +// RestRoomFinderTests +// +// Created by Yilong Chen on 09/06/2024. +// + +import XCTest +@testable import RestRoomFinder + +final class RestRoomFinderTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderUITests/RestRoomFinderUITests.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderUITests/RestRoomFinderUITests.swift new file mode 100755 index 0000000..e28b2b2 --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderUITests/RestRoomFinderUITests.swift @@ -0,0 +1,41 @@ +// +// RestRoomFinderUITests.swift +// RestRoomFinderUITests +// +// Created by Yilong Chen on 09/06/2024. +// + +import XCTest + +final class RestRoomFinderUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderUITests/RestRoomFinderUITestsLaunchTests.swift b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderUITests/RestRoomFinderUITestsLaunchTests.swift new file mode 100755 index 0000000..42de6ce --- /dev/null +++ b/SwiftUI/SwiftUIRedux_RestRoomFinder/RestRoomFinderUITests/RestRoomFinderUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// RestRoomFinderUITestsLaunchTests.swift +// RestRoomFinderUITests +// +// Created by Yilong Chen on 09/06/2024. +// + +import XCTest + +final class RestRoomFinderUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData.xcodeproj/project.pbxproj b/SwiftUI/SwiftUI_CoreData/HelloCoreData.xcodeproj/project.pbxproj new file mode 100755 index 0000000..4c8eb2f --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData.xcodeproj/project.pbxproj @@ -0,0 +1,362 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 8D182DA025D1E37100E05852 /* HelloCoreDataApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D182D9F25D1E37100E05852 /* HelloCoreDataApp.swift */; }; + 8D182DA225D1E37100E05852 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D182DA125D1E37100E05852 /* ContentView.swift */; }; + 8D182DA425D1E37200E05852 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D182DA325D1E37200E05852 /* Assets.xcassets */; }; + 8D182DA725D1E37200E05852 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D182DA625D1E37200E05852 /* Preview Assets.xcassets */; }; + 8D182DB125D1E67300E05852 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D182DB025D1E67300E05852 /* CoreDataManager.swift */; }; + 8D182DB625D1E6C800E05852 /* HelloCoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8D182DB425D1E6C800E05852 /* HelloCoreData.xcdatamodeld */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8D182D9C25D1E37100E05852 /* HelloCoreData.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloCoreData.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D182D9F25D1E37100E05852 /* HelloCoreDataApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloCoreDataApp.swift; sourceTree = ""; }; + 8D182DA125D1E37100E05852 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8D182DA325D1E37200E05852 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8D182DA625D1E37200E05852 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8D182DA825D1E37200E05852 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8D182DB025D1E67300E05852 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + 8D182DB525D1E6C800E05852 /* HelloCoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = HelloCoreData.xcdatamodel; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D182D9925D1E37100E05852 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8D182D9325D1E37100E05852 = { + isa = PBXGroup; + children = ( + 8D182D9E25D1E37100E05852 /* HelloCoreData */, + 8D182D9D25D1E37100E05852 /* Products */, + ); + sourceTree = ""; + }; + 8D182D9D25D1E37100E05852 /* Products */ = { + isa = PBXGroup; + children = ( + 8D182D9C25D1E37100E05852 /* HelloCoreData.app */, + ); + name = Products; + sourceTree = ""; + }; + 8D182D9E25D1E37100E05852 /* HelloCoreData */ = { + isa = PBXGroup; + children = ( + 8D182DAF25D1E66500E05852 /* Managers */, + 8D182D9F25D1E37100E05852 /* HelloCoreDataApp.swift */, + 8D182DA125D1E37100E05852 /* ContentView.swift */, + 8D182DA325D1E37200E05852 /* Assets.xcassets */, + 8D182DA825D1E37200E05852 /* Info.plist */, + 8D182DA525D1E37200E05852 /* Preview Content */, + 8D182DB425D1E6C800E05852 /* HelloCoreData.xcdatamodeld */, + ); + path = HelloCoreData; + sourceTree = ""; + }; + 8D182DA525D1E37200E05852 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8D182DA625D1E37200E05852 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8D182DAF25D1E66500E05852 /* Managers */ = { + isa = PBXGroup; + children = ( + 8D182DB025D1E67300E05852 /* CoreDataManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D182D9B25D1E37100E05852 /* HelloCoreData */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8D182DAB25D1E37200E05852 /* Build configuration list for PBXNativeTarget "HelloCoreData" */; + buildPhases = ( + 8D182D9825D1E37100E05852 /* Sources */, + 8D182D9925D1E37100E05852 /* Frameworks */, + 8D182D9A25D1E37100E05852 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HelloCoreData; + productName = HelloCoreData; + productReference = 8D182D9C25D1E37100E05852 /* HelloCoreData.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8D182D9425D1E37100E05852 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1230; + LastUpgradeCheck = 1230; + TargetAttributes = { + 8D182D9B25D1E37100E05852 = { + CreatedOnToolsVersion = 12.3; + }; + }; + }; + buildConfigurationList = 8D182D9725D1E37100E05852 /* Build configuration list for PBXProject "HelloCoreData" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8D182D9325D1E37100E05852; + productRefGroup = 8D182D9D25D1E37100E05852 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D182D9B25D1E37100E05852 /* HelloCoreData */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D182D9A25D1E37100E05852 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D182DA725D1E37200E05852 /* Preview Assets.xcassets in Resources */, + 8D182DA425D1E37200E05852 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D182D9825D1E37100E05852 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D182DB125D1E67300E05852 /* CoreDataManager.swift in Sources */, + 8D182DB625D1E6C800E05852 /* HelloCoreData.xcdatamodeld in Sources */, + 8D182DA225D1E37100E05852 /* ContentView.swift in Sources */, + 8D182DA025D1E37100E05852 /* HelloCoreDataApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8D182DA925D1E37200E05852 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8D182DAA25D1E37200E05852 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8D182DAC25D1E37200E05852 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"HelloCoreData/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = HelloCoreData/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloCoreData; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8D182DAD25D1E37200E05852 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"HelloCoreData/Preview Content\""; + DEVELOPMENT_TEAM = B2Q8EGNCQA; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = HelloCoreData/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.azamsharp.HelloCoreData; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8D182D9725D1E37100E05852 /* Build configuration list for PBXProject "HelloCoreData" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8D182DA925D1E37200E05852 /* Debug */, + 8D182DAA25D1E37200E05852 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8D182DAB25D1E37200E05852 /* Build configuration list for PBXNativeTarget "HelloCoreData" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8D182DAC25D1E37200E05852 /* Debug */, + 8D182DAD25D1E37200E05852 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 8D182DB425D1E6C800E05852 /* HelloCoreData.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 8D182DB525D1E6C800E05852 /* HelloCoreData.xcdatamodel */, + ); + currentVersion = 8D182DB525D1E6C800E05852 /* HelloCoreData.xcdatamodel */; + path = HelloCoreData.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 8D182D9425D1E37100E05852 /* Project object */; +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100755 index 0000000..eb87897 --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..9221b9b --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/Contents.json b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/ContentView.swift b/SwiftUI/SwiftUI_CoreData/HelloCoreData/ContentView.swift new file mode 100755 index 0000000..3750608 --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/ContentView.swift @@ -0,0 +1,45 @@ +// +// ContentView.swift +// HelloCoreData +// +// Created by Yilong Chen on 2/8/24. +// + +import SwiftUI + +struct ContentView: View { + + let coreDM: CoreDataManager + @State private var movieName: String = "" + @State private var movies: [Movie] = [Movie]() + + private func populateMovies() { + movies = coreDM.getAllMovies() + } + + var body: some View { + VStack { + TextField("Enter movie name", text: $movieName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Button("Save") { + coreDM.saveMovie(name: movieName) + } + + List(movies, id: \.self) { movie in + Text(movie.title!) + }.listStyle(PlainListStyle()) + + Spacer() + + }.padding() + .onAppear(perform: { + populateMovies() + }) + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView(coreDM: CoreDataManager()) + } +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/HelloCoreData.xcdatamodeld/HelloCoreData.xcdatamodel/contents b/SwiftUI/SwiftUI_CoreData/HelloCoreData/HelloCoreData.xcdatamodeld/HelloCoreData.xcdatamodel/contents new file mode 100755 index 0000000..7021bc1 --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/HelloCoreData.xcdatamodeld/HelloCoreData.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/HelloCoreDataApp.swift b/SwiftUI/SwiftUI_CoreData/HelloCoreData/HelloCoreDataApp.swift new file mode 100755 index 0000000..c382e5e --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/HelloCoreDataApp.swift @@ -0,0 +1,17 @@ +// +// HelloCoreDataApp.swift +// HelloCoreData +// +// Created by Yilong Chen on 2/8/24. +// + +import SwiftUI + +@main +struct HelloCoreDataApp: App { + var body: some Scene { + WindowGroup { + ContentView(coreDM: CoreDataManager()) + } + } +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/Info.plist b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Info.plist new file mode 100755 index 0000000..efc211a --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/Managers/CoreDataManager.swift b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Managers/CoreDataManager.swift new file mode 100755 index 0000000..e9b1c1a --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Managers/CoreDataManager.swift @@ -0,0 +1,51 @@ +// +// CoreDataManager.swift +// HelloCoreData +// +// Created by Yilong Chen on 2/8/24. +// + +import CoreData + +class CoreDataManager { + + let persistentContainer: NSPersistentContainer + + init() { + persistentContainer = NSPersistentContainer(name: "HelloCoreData") + persistentContainer.loadPersistentStores { (description, error) in + if let error = error { + fatalError("Core Data store failed to initialize \(error.localizedDescription)") + } + } + + let dirPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + print(dirPaths[0]) + } + + func getAllMovies() -> [Movie] { + + let fetchRequest: NSFetchRequest = Movie.fetchRequest() + + do { + return try persistentContainer.viewContext.fetch(fetchRequest) + } catch { + return [] + } + } + + func saveMovie(name: String) { + + let movie = Movie(context: persistentContainer.viewContext) + + movie.title = name + + do { + try persistentContainer.viewContext.save() + print("Movie Saved Successfully") + } catch { + print("Failed to save movie \(error)") + } + } + +} diff --git a/SwiftUI/SwiftUI_CoreData/HelloCoreData/Preview Content/Preview Assets.xcassets/Contents.json b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/SwiftUI/SwiftUI_CoreData/HelloCoreData/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/assets/MVVM_Clean_SOLID/banner_cn.jpg b/assets/MVVM_Clean_SOLID/banner_cn.jpg deleted file mode 100755 index 67a3c63..0000000 Binary files a/assets/MVVM_Clean_SOLID/banner_cn.jpg and /dev/null differ diff --git a/assets/MVVM_Clean_SOLID/banner_cn.png b/assets/MVVM_Clean_SOLID/banner_cn.png new file mode 100644 index 0000000..3f6286f Binary files /dev/null and b/assets/MVVM_Clean_SOLID/banner_cn.png differ diff --git a/assets/MVVM_Clean_SOLID/banner_en.jpg b/assets/MVVM_Clean_SOLID/banner_en.jpg deleted file mode 100755 index 57966d6..0000000 Binary files a/assets/MVVM_Clean_SOLID/banner_en.jpg and /dev/null differ diff --git a/assets/MVVM_Clean_SOLID/banner_en.png b/assets/MVVM_Clean_SOLID/banner_en.png new file mode 100644 index 0000000..23d3c45 Binary files /dev/null and b/assets/MVVM_Clean_SOLID/banner_en.png differ diff --git a/assets/MVVM_Clean_SOLID/cn.jpg b/assets/MVVM_Clean_SOLID/cn.jpg deleted file mode 100755 index 81521e2..0000000 Binary files a/assets/MVVM_Clean_SOLID/cn.jpg and /dev/null differ diff --git a/assets/MVVM_Clean_SOLID/cn.png b/assets/MVVM_Clean_SOLID/cn.png new file mode 100644 index 0000000..de6a5d6 Binary files /dev/null and b/assets/MVVM_Clean_SOLID/cn.png differ diff --git a/assets/MVVM_Clean_SOLID/en.jpg b/assets/MVVM_Clean_SOLID/en.jpg deleted file mode 100755 index 003f8f6..0000000 Binary files a/assets/MVVM_Clean_SOLID/en.jpg and /dev/null differ diff --git a/assets/MVVM_Clean_SOLID/en.png b/assets/MVVM_Clean_SOLID/en.png new file mode 100644 index 0000000..f601787 Binary files /dev/null and b/assets/MVVM_Clean_SOLID/en.png differ diff --git a/assets/MVVM_Clean_SOLID/tca_horizontal_layer.png b/assets/MVVM_Clean_SOLID/tca_horizontal_layer.png new file mode 100644 index 0000000..c4074cb Binary files /dev/null and b/assets/MVVM_Clean_SOLID/tca_horizontal_layer.png differ diff --git a/assets/MVVM_Clean_SOLID/tca_onion_layering.png b/assets/MVVM_Clean_SOLID/tca_onion_layering.png new file mode 100644 index 0000000..9561666 Binary files /dev/null and b/assets/MVVM_Clean_SOLID/tca_onion_layering.png differ diff --git a/assets/Property_Wrapper_in_SwiftUI/ios16_code.png b/assets/Property_Wrapper_in_SwiftUI/ios16_code.png new file mode 100755 index 0000000..208389f Binary files /dev/null and b/assets/Property_Wrapper_in_SwiftUI/ios16_code.png differ diff --git a/assets/Property_Wrapper_in_SwiftUI/ios16_overview.png b/assets/Property_Wrapper_in_SwiftUI/ios16_overview.png new file mode 100755 index 0000000..d0c7ef9 Binary files /dev/null and b/assets/Property_Wrapper_in_SwiftUI/ios16_overview.png differ diff --git a/assets/Property_Wrapper_in_SwiftUI/ios16_pros_cons.png b/assets/Property_Wrapper_in_SwiftUI/ios16_pros_cons.png new file mode 100755 index 0000000..92e2b7e Binary files /dev/null and b/assets/Property_Wrapper_in_SwiftUI/ios16_pros_cons.png differ diff --git a/assets/Property_Wrapper_in_SwiftUI/ios17_code.png b/assets/Property_Wrapper_in_SwiftUI/ios17_code.png new file mode 100755 index 0000000..27376d1 Binary files /dev/null and b/assets/Property_Wrapper_in_SwiftUI/ios17_code.png differ diff --git a/assets/Property_Wrapper_in_SwiftUI/ios17_overview.png b/assets/Property_Wrapper_in_SwiftUI/ios17_overview.png new file mode 100755 index 0000000..c7c30e4 Binary files /dev/null and b/assets/Property_Wrapper_in_SwiftUI/ios17_overview.png differ diff --git a/assets/Property_Wrapper_in_SwiftUI/ios17_pros_cons.png b/assets/Property_Wrapper_in_SwiftUI/ios17_pros_cons.png new file mode 100755 index 0000000..a2cd854 Binary files /dev/null and b/assets/Property_Wrapper_in_SwiftUI/ios17_pros_cons.png differ diff --git a/assets/TCA_Each_View_has_its_Store_architecture_design_pattern.png b/assets/TCA_Each_View_has_its_Store_architecture_design_pattern.png new file mode 100644 index 0000000..3d7c402 Binary files /dev/null and b/assets/TCA_Each_View_has_its_Store_architecture_design_pattern.png differ diff --git a/assets/TCA_schema_architecture_design_pattern.jpg b/assets/TCA_schema_architecture_design_pattern.jpg new file mode 100644 index 0000000..c97a232 Binary files /dev/null and b/assets/TCA_schema_architecture_design_pattern.jpg differ diff --git a/assets/redux_architecture_design_pattern.graffle b/assets/redux_architecture_design_pattern.graffle new file mode 100644 index 0000000..96a5b98 Binary files /dev/null and b/assets/redux_architecture_design_pattern.graffle differ diff --git a/assets/redux_architecture_design_pattern.png b/assets/redux_architecture_design_pattern.png new file mode 100644 index 0000000..97a53ac Binary files /dev/null and b/assets/redux_architecture_design_pattern.png differ diff --git a/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_QA.png b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_QA.png new file mode 100755 index 0000000..7bf6622 Binary files /dev/null and b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_QA.png differ diff --git a/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy1.png b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy1.png new file mode 100755 index 0000000..cfb1ba2 Binary files /dev/null and b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy1.png differ diff --git a/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy2.png b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy2.png new file mode 100755 index 0000000..563722d Binary files /dev/null and b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_copy2.png differ diff --git a/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_en.png b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_en.png new file mode 100755 index 0000000..02821c5 Binary files /dev/null and b/assets/redux_architecture_design_pattern/redux_architecture_design_pattern_en.png differ diff --git a/assets/redux_architecture_design_pattern_copy.graffle b/assets/redux_architecture_design_pattern_copy.graffle new file mode 100644 index 0000000..af58e0e Binary files /dev/null and b/assets/redux_architecture_design_pattern_copy.graffle differ diff --git a/assets/redux_architecture_design_pattern_copy.png b/assets/redux_architecture_design_pattern_copy.png new file mode 100644 index 0000000..e55d16f Binary files /dev/null and b/assets/redux_architecture_design_pattern_copy.png differ diff --git a/assets/redux_swiftui_architecture_design_pattern.graffle b/assets/redux_swiftui_architecture_design_pattern.graffle new file mode 100644 index 0000000..7e0c911 Binary files /dev/null and b/assets/redux_swiftui_architecture_design_pattern.graffle differ diff --git a/assets/redux_swiftui_architecture_design_pattern.png b/assets/redux_swiftui_architecture_design_pattern.png new file mode 100644 index 0000000..7d2b271 Binary files /dev/null and b/assets/redux_swiftui_architecture_design_pattern.png differ diff --git a/assets/tca_architecture_design_pattern.graffle b/assets/tca_architecture_design_pattern.graffle index 52966c0..afe6c63 100644 Binary files a/assets/tca_architecture_design_pattern.graffle and b/assets/tca_architecture_design_pattern.graffle differ diff --git a/assets/tca_architecture_design_pattern.png b/assets/tca_architecture_design_pattern.png index 3e7623e..9d2ac3f 100644 Binary files a/assets/tca_architecture_design_pattern.png and b/assets/tca_architecture_design_pattern.png differ diff --git a/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_QA.jpg b/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_QA.jpg deleted file mode 100755 index db3806b..0000000 Binary files a/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_QA.jpg and /dev/null differ diff --git a/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_QA.png b/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_QA.png new file mode 100755 index 0000000..e5c50a0 Binary files /dev/null and b/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_QA.png differ diff --git a/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.jpg b/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.jpg deleted file mode 100755 index 44bef78..0000000 Binary files a/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.jpg and /dev/null differ diff --git a/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.png b/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.png new file mode 100755 index 0000000..8bdb758 Binary files /dev/null and b/assets/tca_architecture_design_pattern/tca_architecture_design_pattern_en.png differ