diff --git a/Example/Example/SceneDelegate.swift b/Example/Example/SceneDelegate.swift index 46ed832..ea3362f 100644 --- a/Example/Example/SceneDelegate.swift +++ b/Example/Example/SceneDelegate.swift @@ -26,7 +26,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { }() navigationController.viewControllers = [ - FirstView(viewModel: FirstViewModel()).viewController, + FirstView(stateView: .init()).viewController, NumberingView(viewModel: .init()).viewController, NumberingView(viewModel: .init()).viewController ] diff --git a/Example/Example/View/ContentView.swift b/Example/Example/View/ContentView.swift index d91f526..10d6977 100644 --- a/Example/Example/View/ContentView.swift +++ b/Example/Example/View/ContentView.swift @@ -9,36 +9,32 @@ import ControllerableViewModel import SwiftUI -final class FirstViewModel: NavigationableViewModel { - -} - struct FirstView: ControllerableView { - @ObservedObject var viewModel: FirstViewModel + var stateView: StateView var body: some View { VStack(spacing: 30) { Button(action: { - viewModel.push(view: NumberingView(viewModel: .init())) + self.push(view: NumberingView(viewModel: .init())) }) { Text("Push") } Button(action: { - viewModel.present(view: NumberingView(viewModel: .init())) + self.present(view: NumberingView(viewModel: .init())) }) { Text("Default Present") } Button(action: { - viewModel.present(view: NumberingView(viewModel: .init()), to: .fullScreen, by: .coverVertical) + self.present(view: NumberingView(viewModel: .init()), to: .fullScreen, by: .coverVertical) }) { Text("Full Screen") } Button(action: { - viewModel.present(view: NumberingView(viewModel: .init()), to: .custom, by: .coverVertical, with: [.large(), .medium()]) + self.present(view: NumberingView(viewModel: .init()), to: .custom, by: .coverVertical, with: [.large(), .medium()]) }) { Text("Changable Modal") } diff --git a/Example/Example/View/CustomAlertView.swift b/Example/Example/View/CustomAlertView.swift index b16fda5..64442e5 100644 --- a/Example/Example/View/CustomAlertView.swift +++ b/Example/Example/View/CustomAlertView.swift @@ -9,28 +9,16 @@ import ControllerableViewModel import SwiftUI -final class CustomeAlertViewModel: NavigationableViewModel { - - struct Item { - let title: String - let message: String - let completion: () -> Void - } - - let item: Item - - init(item: Item) { - self.item = item - } - - var title: String { item.title } - var message: String { item.message } - var completion: () -> Void { return item.completion } +struct AlertItem { + let title: String + let message: String + let completion: () -> Void } struct CustomAlert: ControllerableView { - @ObservedObject var viewModel: CustomeAlertViewModel + var stateView: StateView + let item : AlertItem var body: some View { ZStack { @@ -39,10 +27,10 @@ struct CustomAlert: ControllerableView { .ignoresSafeArea(.all) VStack(alignment: .center, spacing: 0) { Spacer() - Text(viewModel.title) + Text(item.title) Spacer() .frame(height: 11) - Text(viewModel.message) + Text(item.message) .foregroundColor(.gray) Spacer() Divider() @@ -50,8 +38,8 @@ struct CustomAlert: ControllerableView { Spacer() Button( action: { - viewModel.dismiss() - viewModel.completion() + self.dismiss() + item.completion() }, label: { HStack { Spacer() diff --git a/Example/Example/View/NumberingView.swift b/Example/Example/View/NumberingView.swift index 1a58ef2..311c759 100644 --- a/Example/Example/View/NumberingView.swift +++ b/Example/Example/View/NumberingView.swift @@ -9,16 +9,14 @@ import ControllerableViewModel import SwiftUI -final class NumberingViewModel: NavigationableViewModel { +final class NumberingViewModel: ObservableObject { static var number: Int = 0 let number: Int - override init() { + init() { self.number = Self.number - - super.init() - + Self.number += 1 print("init \(number)") @@ -32,6 +30,8 @@ final class NumberingViewModel: NavigationableViewModel { struct NumberingView: ControllerableView { + var stateView: StateView = .init() + @ObservedObject var viewModel: NumberingViewModel var body: some View { @@ -40,67 +40,65 @@ struct NumberingView: ControllerableView { Text("Number \(viewModel.number)") Button(action: { - viewModel.push(view: NumberingView(viewModel: .init())) + self.push(view: NumberingView(viewModel: .init())) }) { Text("push") } Button(action: { - viewModel.present(view: NumberingView(viewModel: .init())) + self.present(view: NumberingView(viewModel: .init())) }) { Text("Default Present") } Button(action: { - viewModel.present(view: NumberingView(viewModel: .init()), to: .fullScreen, by: .coverVertical) + self.present(view: NumberingView(viewModel: .init()), to: .fullScreen, by: .coverVertical) }) { Text("Full Screen") } Button(action: { - viewModel.present(view: NumberingView(viewModel: .init()), to: .custom, by: .coverVertical, with: [.large(), .medium()]) + self.present(view: NumberingView(viewModel: .init()), to: .custom, by: .coverVertical, with: [.large(), .medium()]) }) { Text("Changable Modal") } Button(action: { - viewModel.dismiss() + self.dismiss() }) { Text("dismiss") } Button(action: { - viewModel.pop() + self.pop() }) { Text("pop") } Button(action: { - viewModel.popToRoot() + self.popToRoot() }) { Text("pop to root") } Button(action: { - viewModel.popToRoot() - viewModel.push(view: NumberingView(viewModel: .init())) - viewModel.push(view: NumberingView(viewModel: .init())) + self.popToRoot() + self.push(view: NumberingView(viewModel: .init())) + self.push(view: NumberingView(viewModel: .init())) }) { Text("reStacks") } Button(action: { - let viewModel = CustomeAlertViewModel( - item: .init( - title: "SwiftUI 로 구현된 Alert 예제", - message: "UIKit 으로도 구현가능\n여기는 View Number \(viewModel.number)", - completion: { - print("얼럿 Completion") - } - ) + let alertItem = AlertItem( + title: "SwiftUI 로 구현된 Alert 예제", + message: "UIKit 으로도 구현가능\n여기는 View Number \(viewModel.number)", + completion: { + print("얼럿 Completion") + } ) - let view = CustomAlert(viewModel: viewModel) - viewModel.alert(view: view) + let view = CustomAlert(stateView: .init(), item: alertItem) + self.presentAlert(view: view) }) { Text("alert") } diff --git a/Sources/ControllerableViewModel/NavigationableViewModel.swift b/Sources/ControllerableViewModel/ControllerableView+Extension.swift similarity index 66% rename from Sources/ControllerableViewModel/NavigationableViewModel.swift rename to Sources/ControllerableViewModel/ControllerableView+Extension.swift index ffef506..55b68ac 100644 --- a/Sources/ControllerableViewModel/NavigationableViewModel.swift +++ b/Sources/ControllerableViewModel/ControllerableView+Extension.swift @@ -1,35 +1,38 @@ // -// NavigationableViewModel.swift +// File.swift +// // -// -// Created by 김용우 on 2023/10/17. +// Created by 김용우 on 2023/10/19. // import SwiftUI -open class NavigationableViewModel: ObservableObject { - public weak var viewController: UIViewController? +public extension ControllerableView { - public init() {} -} - -// MARK: - Navigation -@MainActor -extension NavigationableViewModel { + var viewController: UIViewController { + let viewController = HostingController(rootView: self) + self.stateView.viewController = viewController + return viewController + } + + // MARK: ViewController Life Cycle + func loadView() {} + func viewDidLoad() {} + func viewWillAppear() {} + func viewDidAppear() {} + func viewWillDisappear() {} + func viewDidDisappear() {} - public func push(view: some ControllerableView) { - viewController?.navigationController?.pushViewController(view.viewController, animated: true) + // MARK: Navigation + func push(view: some ControllerableView) { + stateView.viewController?.navigationController?.pushViewController(view.viewController, animated: true) } - public func pop() { - guard let currentViewController = viewController, - let navigationController = currentViewController.navigationController else { return } - guard navigationController.topViewController == currentViewController else { return } - - navigationController.popViewController(animated: true) + func pop() { + stateView.viewController?.navigationController?.popViewController(animated: true) } - public func present( + func present( view: some ControllerableView, to presentationStyle: UIModalPresentationStyle? = nil, by transitionStyle: UIModalTransitionStyle? = nil, @@ -50,22 +53,17 @@ extension NavigationableViewModel { let presentingViewController = UINavigationController(rootViewController: nextViewController) presentingViewController.setNavigationBarHidden(true, animated: false) - viewController?.present(presentingViewController, animated: true) + stateView.viewController?.present(presentingViewController, animated: true) } - public func dismiss() { - viewController?.dismiss(animated: true) + func dismiss() { + stateView.viewController?.dismiss(animated: true) } - public func popToRoot() { - viewController?.navigationController?.popToRootViewController(animated: true) + func popToRoot() { + stateView.viewController?.navigationController?.popToRootViewController(animated: true) } -} - -@MainActor -extension NavigationableViewModel { - private var window: UIWindow? { if #available(iOS 16.0, *) { return UIApplication.shared.connectedScenes @@ -77,7 +75,7 @@ extension NavigationableViewModel { } } - public func alert( + func presentAlert( view: some ControllerableView, to presentationStyle: UIModalPresentationStyle? = .overFullScreen, by transitionStyle: UIModalTransitionStyle? = .crossDissolve @@ -101,5 +99,4 @@ extension NavigationableViewModel { rootViewController.present(nextViewController, animated: true) } - } diff --git a/Sources/ControllerableViewModel/ControllerableView.swift b/Sources/ControllerableViewModel/ControllerableView.swift index 88064f6..87ee70b 100644 --- a/Sources/ControllerableViewModel/ControllerableView.swift +++ b/Sources/ControllerableViewModel/ControllerableView.swift @@ -8,10 +8,9 @@ import SwiftUI public protocol ControllerableView: View { - associatedtype ViewModel - - var viewModel: ViewModel { get set } + var stateView: StateView { get set } + // MARK: ViewController Life Cycle func loadView() func viewDidLoad() @@ -19,56 +18,13 @@ public protocol ControllerableView: View { func viewDidAppear() func viewWillDisappear() func viewDidDisappear() -} - -public extension ControllerableView { - - var viewController: UIViewController { - let viewController = HostingController(rootView: self) - (self.viewModel as? NavigationableViewModel)?.viewController = viewController - return viewController - } - - // MARK: ViewController Life Cycle - func loadView() {} - func viewDidLoad() {} - func viewWillAppear() {} - func viewDidAppear() {} - func viewWillDisappear() {} - func viewDidDisappear() {} -} - -public class HostingController: UIHostingController { - - public override func loadView() { - super.loadView() - self.rootView.loadView() - } - - public override func viewDidLoad() { - super.viewDidLoad() - self.rootView.viewDidLoad() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.rootView.viewWillAppear() - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - self.rootView.viewDidAppear() - } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - self.rootView.viewWillDisappear() - } - - public override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - self.rootView.viewDidDisappear() - } + // MARK: Navigation + func push(view: some ControllerableView) + func pop() + func present(view: some ControllerableView, to presentationStyle: UIModalPresentationStyle?, by transitionStyle: UIModalTransitionStyle?, with detents: [UISheetPresentationController.Detent]?) + func dismiss() + func popToRoot() + func presentAlert(view: some ControllerableView, to presentationStyle: UIModalPresentationStyle?, by transitionStyle: UIModalTransitionStyle?) } diff --git a/Sources/ControllerableViewModel/HostingController.swift b/Sources/ControllerableViewModel/HostingController.swift new file mode 100644 index 0000000..5754a88 --- /dev/null +++ b/Sources/ControllerableViewModel/HostingController.swift @@ -0,0 +1,42 @@ +// +// HostingController.swift +// +// +// Created by 김용우 on 2023/10/19. +// + +import SwiftUI + +public class HostingController: UIHostingController { + + public override func loadView() { + super.loadView() + self.rootView.loadView() + } + + public override func viewDidLoad() { + super.viewDidLoad() + self.rootView.viewDidLoad() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.rootView.viewWillAppear() + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.rootView.viewDidAppear() + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.rootView.viewWillDisappear() + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.rootView.viewDidDisappear() + } + +} diff --git a/Sources/ControllerableViewModel/StateView.swift b/Sources/ControllerableViewModel/StateView.swift new file mode 100644 index 0000000..f11167d --- /dev/null +++ b/Sources/ControllerableViewModel/StateView.swift @@ -0,0 +1,14 @@ +// +// StateView.swift +// +// +// Created by 김용우 on 2023/10/17. +// + +import SwiftUI + +open class StateView { + public weak var viewController: UIViewController? + + public init() {} +} diff --git a/Tests/ControllerableViewModelTests/ControllerableViewModelTests.swift b/Tests/ControllerableViewModelTests/ControllerableViewModelTests.swift index 0174a6f..3626c14 100644 --- a/Tests/ControllerableViewModelTests/ControllerableViewModelTests.swift +++ b/Tests/ControllerableViewModelTests/ControllerableViewModelTests.swift @@ -6,6 +6,6 @@ final class ControllerableViewModelTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(ControllerableViewModel().text, "Hello, World!") + XCTAssertEqual("Hello, World!", "Hello, World!") } }