diff --git a/README.md b/README.md index 2fb8bf1..bacae6b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SwiftUITodo is an example to-do list application using [SwiftUI](https://develop ## Requirements -* Xcode 11 Beta +* Xcode 11 Beta 6 * Swift 5.1 ## License diff --git a/SwiftUITodo/AppDelegate.swift b/SwiftUITodo/AppDelegate.swift index 915ca17..00ea096 100644 --- a/SwiftUITodo/AppDelegate.swift +++ b/SwiftUITodo/AppDelegate.swift @@ -10,32 +10,32 @@ 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 - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - // 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. - } - - + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + // 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/SwiftUITodo/SceneDelegate.swift b/SwiftUITodo/SceneDelegate.swift index 443dfb1..41d19f6 100644 --- a/SwiftUITodo/SceneDelegate.swift +++ b/SwiftUITodo/SceneDelegate.swift @@ -10,52 +10,56 @@ import UIKit import SwiftUI 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). - - // Use a UIHostingController as window root view controller - let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = UIHostingController(rootView: NavigationView { - TaskListView().environmentObject(UserData()) - }) - 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. - } - - + + 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). + + let rootView = NavigationView { + TaskListView().environmentObject(UserData()) + } + + // Use a UIHostingController as window root view controller + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + window.rootViewController = UIHostingController(rootView: rootView) + 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/SwiftUITodo/Task.swift b/SwiftUITodo/Task.swift index 9f83bb0..3a7590e 100644 --- a/SwiftUITodo/Task.swift +++ b/SwiftUITodo/Task.swift @@ -9,13 +9,13 @@ import SwiftUI struct Task: Equatable, Hashable, Codable, Identifiable { - let id: UUID - var title: String - var isDone: Bool - - init(title: String, isDone: Bool) { - self.id = UUID() - self.title = title - self.isDone = isDone - } + let id: UUID + var title: String + var isDone: Bool + + init(title: String, isDone: Bool) { + self.id = UUID() + self.title = title + self.isDone = isDone + } } diff --git a/SwiftUITodo/TaskEditView.swift b/SwiftUITodo/TaskEditView.swift index 81d3da2..e23fda1 100644 --- a/SwiftUITodo/TaskEditView.swift +++ b/SwiftUITodo/TaskEditView.swift @@ -9,44 +9,46 @@ import SwiftUI struct TaskEditView: View { - @EnvironmentObject var userData: UserData - private let task: Task - private var draftTitle: State + @EnvironmentObject var userData: UserData + private let task: Task + private var draftTitle: State - init(task: Task) { - self.task = task - self.draftTitle = .init(initialValue: task.title) - } - - var body: some View { - let inset = EdgeInsets(top: -8, leading: -10, bottom: -7, trailing: -10) - return VStack(alignment: .leading, spacing: 0) { - TextField( - self.draftTitle.binding, - placeholder: Text("Enter New Title..."), - onEditingChanged: { _ in self.updateTask() }, - onCommit: {} - ) - .background( - RoundedRectangle(cornerRadius: 5) - .fill(Color.clear) - .border(Color(red: 0.7, green: 0.7, blue: 0.7), width: 1 / UIScreen.main.scale, cornerRadius: 5) - .padding(inset) - ) - .padding(EdgeInsets( - top: 15 - inset.top, - leading: 20 - inset.leading, - bottom: 15 - inset.bottom, - trailing: 20 - inset.trailing - )) - - Spacer() + + init(task: Task) { + self.task = task + self.draftTitle = .init(initialValue: task.title) + } + + var body: some View { + let inset = EdgeInsets(top: -8, leading: -10, bottom: -7, trailing: -10) + return VStack { + TextField( + "Enter New Title...", + text: draftTitle.projectedValue, + onEditingChanged: { _ in self.updateTask() }, + onCommit: {} + ) + .background( + RoundedRectangle(cornerRadius: 5) + .fill(Color.clear) + .border(Color(red: 0.7, green: 0.7, blue: 0.7), width: 1 / UIScreen.main.scale) + .cornerRadius(5) + .padding(inset) + ) + .padding(EdgeInsets( + top: 15 - inset.top, + leading: 20 - inset.leading, + bottom: 15 - inset.bottom, + trailing: 20 - inset.trailing + )) + + Spacer() + } + .navigationBarTitle(Text("Edit Task 📝")) + } + + private func updateTask() { + guard let index = self.userData.tasks.firstIndex(of: self.task) else { return } + self.userData.tasks[index].title = self.draftTitle.wrappedValue } - .navigationBarTitle(Text("Edit Task 📝")) - } - - private func updateTask() { - guard let index = self.userData.tasks.firstIndex(of: self.task) else { return } - self.userData.tasks[index].title = self.draftTitle.value - } } diff --git a/SwiftUITodo/TaskItemView.swift b/SwiftUITodo/TaskItemView.swift index e9f9fdd..a19dab0 100644 --- a/SwiftUITodo/TaskItemView.swift +++ b/SwiftUITodo/TaskItemView.swift @@ -9,44 +9,45 @@ import SwiftUI struct TaskItemView: View { - @EnvironmentObject var userData: UserData - - let task: Task - @Binding var isEditing: Bool - - var body: some View { - return HStack { - if self.isEditing { - Image(systemName: "minus.circle") - .foregroundColor(.red) - .tapAction(count: 1) { - self.delete() - } - NavigationButton(destination: TaskEditView(task: task).environmentObject(self.userData)) { - Text(task.title) - } - } else { - Button(action: { self.toggleDone() }) { - Text(self.task.title) - } - Spacer() - if task.isDone { - Image(systemName: "checkmark").foregroundColor(.green) + @EnvironmentObject var userData: UserData + + let task: Task + @Binding var isEditing: Bool + + var body: some View { + HStack { + if self.isEditing { + Image(systemName: "minus.circle") + .foregroundColor(.red) + .onTapGesture(count: 1) { + self.delete() + } + + NavigationLink(destination: TaskEditView(task: task).environmentObject(self.userData)) { + Text(task.title) + } + } else { + Button(action: { self.toggleDone() }) { + Text(self.task.title) + } + Spacer() + if task.isDone { + Image(systemName: "checkmark").foregroundColor(.green) + } + } } - } } - } - - private func toggleDone() { - guard !self.isEditing else { return } - guard let index = self.userData.tasks.firstIndex(where: { $0.id == self.task.id }) else { return } - self.userData.tasks[index].isDone.toggle() - } - - private func delete() { - self.userData.tasks.removeAll(where: { $0.id == self.task.id }) - if self.userData.tasks.isEmpty { - self.isEditing = false + + private func toggleDone() { + guard !self.isEditing else { return } + guard let index = self.userData.tasks.firstIndex(where: { $0.id == self.task.id }) else { return } + self.userData.tasks[index].isDone.toggle() + } + + private func delete() { + self.userData.tasks.removeAll(where: { $0.id == self.task.id }) + if self.userData.tasks.isEmpty { + self.isEditing = false + } } - } } diff --git a/SwiftUITodo/TaskListView.swift b/SwiftUITodo/TaskListView.swift index d76caf9..2c676fd 100644 --- a/SwiftUITodo/TaskListView.swift +++ b/SwiftUITodo/TaskListView.swift @@ -9,30 +9,30 @@ import SwiftUI struct TaskListView: View { - @EnvironmentObject var userData: UserData - @State var draftTitle: String = "" - @State var isEditing: Bool = false - - var body: some View { - List { - TextField($draftTitle, placeholder: Text("Create a New Task..."), onCommit: self.createTask) - ForEach(self.userData.tasks) { task in - TaskItemView(task: task, isEditing: self.$isEditing) - } + @EnvironmentObject var userData: UserData + @State var draftTitle: String = "" + @State var isEditing: Bool = false + + var body: some View { + List { + TextField("Create a New Task...", text: $draftTitle, onCommit: self.createTask) + ForEach(self.userData.tasks) { task in + TaskItemView(task: task, isEditing: self.$isEditing) + } + } + .navigationBarTitle(Text("Tasks 👀")) + .navigationBarItems(trailing: Button(action: { self.isEditing.toggle() }) { + if !self.isEditing { + Text("Edit") + } else { + Text("Done").bold() + } + }) + } + + private func createTask() { + let newTask = Task(title: self.draftTitle, isDone: false) + self.userData.tasks.insert(newTask, at: 0) + self.draftTitle = "" } - .navigationBarTitle(Text("Tasks 👀")) - .navigationBarItems(trailing: Button(action: { self.isEditing.toggle() }) { - if !self.isEditing { - Text("Edit") - } else { - Text("Done").bold() - } - }) - } - - private func createTask() { - let newTask = Task(title: self.draftTitle, isDone: false) - self.userData.tasks.insert(newTask, at: 0) - self.draftTitle = "" - } } diff --git a/SwiftUITodo/UserData.swift b/SwiftUITodo/UserData.swift index 368e996..b135e87 100644 --- a/SwiftUITodo/UserData.swift +++ b/SwiftUITodo/UserData.swift @@ -10,17 +10,23 @@ import Combine import SwiftUI private let defaultTasks: [Task] = [ - Task(title: "Read SwiftUI Documentation 📚", isDone: false), - Task(title: "Watch WWDC19 Keynote 🎉", isDone: true), + Task(title: "Read SwiftUI Documentation 📚", isDone: false), + Task(title: "Watch WWDC19 Keynote 🎉", isDone: true), ] -final class UserData: BindableObject { - let didChange = PassthroughSubject() - - @UserDefaultValue(key: "Tasks", defaultValue: defaultTasks) - var tasks: [Task] { - didSet { - didChange.send(self) +final class UserData: ObservableObject { + + @Published var tasks = [Task]() { + didSet { + // Update to user default + userDefaultTasks = tasks + } + } + + @UserDefaultValue(key: "Tasks", defaultValue: defaultTasks) + private var userDefaultTasks: [Task] + + init() { + tasks = self.userDefaultTasks } - } } diff --git a/SwiftUITodo/UserDefaultsValue.swift b/SwiftUITodo/UserDefaultsValue.swift index 73be39c..02bbd07 100644 --- a/SwiftUITodo/UserDefaultsValue.swift +++ b/SwiftUITodo/UserDefaultsValue.swift @@ -8,13 +8,13 @@ import Foundation -@propertyDelegate +@propertyWrapper struct UserDefaultValue { - + let key: String let defaultValue: Value - - var value: Value { + + var wrappedValue: Value { get { let data = UserDefaults.standard.data(forKey: key) let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }