Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] SwiftUIScreen with action sink #225

Closed
wants to merge 9 commits into from
Closed
2 changes: 2 additions & 0 deletions Development.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Pod::Spec.new do |s|
app_spec.dependency 'BackStackContainer'
app_spec.dependency 'ModalContainer'
app_spec.dependency 'AlertContainer'
app_spec.dependency 'WorkflowSwiftUI'
end

s.test_spec 'TicTacToeTests' do |test_spec|
Expand All @@ -72,6 +73,7 @@ Pod::Spec.new do |s|
test_spec.dependency 'BackStackContainer'
test_spec.dependency 'ModalContainer'
test_spec.dependency 'AlertContainer'
test_spec.dependency 'WorkflowSwiftUI'
test_spec.requires_app_host = true
test_spec.app_host_name = 'Development/SampleTicTacToe'
test_spec.source_files = 'Samples/TicTacToe/Tests/**/*.swift'
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ let package = Package(
),
.target(
name: "WorkflowSwiftUI",
dependencies: ["Workflow"],
dependencies: ["Workflow", "WorkflowUI"],
path: "WorkflowSwiftUI/Sources"
),

Expand Down
146 changes: 30 additions & 116 deletions Samples/TicTacToe/Sources/Authentication/LoginScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,128 +14,42 @@
* limitations under the License.
*/

import SwiftUI
import Workflow
import WorkflowUI
import WorkflowSwiftUI

struct LoginScreen: Screen {
struct LoginScreen: SwiftUIScreen, Equatable {
var actionSink: ScreenActionSink<LoginWorkflow.Action>
var title: String
var email: String
var onEmailChanged: (String) -> Void
var password: String
var onPasswordChanged: (String) -> Void
var onLoginTapped: () -> Void

func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
return ViewControllerDescription(
environment: environment,
build: { LoginViewController() },
update: { $0.update(with: self) }
)
}
}

private final class LoginViewController: UIViewController {
private let welcomeLabel: UILabel = UILabel(frame: .zero)
private let emailField: UITextField = UITextField(frame: .zero)
private let passwordField: UITextField = UITextField(frame: .zero)
private let button: UIButton = UIButton(frame: .zero)
private var onEmailChanged: (String) -> Void = { _ in }
private var onPasswordChanged: (String) -> Void = { _ in }
private var onLoginTapped: () -> Void = {}

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .white

welcomeLabel.textAlignment = .center

emailField.placeholder = "[email protected]"
emailField.autocapitalizationType = .none
emailField.autocorrectionType = .no
emailField.textContentType = .emailAddress
emailField.backgroundColor = UIColor(white: 0.92, alpha: 1.0)
emailField.addTarget(self, action: #selector(textDidChange(sender:)), for: .editingChanged)

passwordField.placeholder = "password"
passwordField.isSecureTextEntry = true
passwordField.backgroundColor = UIColor(white: 0.92, alpha: 1.0)
passwordField.addTarget(self, action: #selector(textDidChange(sender:)), for: .editingChanged)

button.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0)
button.setTitle("Login", for: .normal)
button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside)

view.addSubview(welcomeLabel)
view.addSubview(emailField)
view.addSubview(passwordField)
view.addSubview(button)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

let inset: CGFloat = 12.0
let height: CGFloat = 44.0
var yOffset = (view.bounds.size.height - (3 * height + inset)) / 2.0

welcomeLabel.frame = CGRect(
x: view.bounds.origin.x,
y: view.bounds.origin.y,
width: view.bounds.size.width,
height: yOffset
)

emailField.frame = CGRect(
x: view.bounds.origin.x,
y: yOffset,
width: view.bounds.size.width,
height: height
)
.insetBy(dx: inset, dy: 0.0)

yOffset += height + inset

passwordField.frame = CGRect(
x: view.bounds.origin.x,
y: yOffset,
width: view.bounds.size.width,
height: height
)
.insetBy(dx: inset, dy: 0.0)

yOffset += height + inset

button.frame = CGRect(
x: view.bounds.origin.x,
y: yOffset,
width: view.bounds.size.width,
height: height
)
.insetBy(dx: inset, dy: 0.0)
}

func update(with screen: LoginScreen) {
welcomeLabel.text = screen.title
emailField.text = screen.email
passwordField.text = screen.password
onEmailChanged = screen.onEmailChanged
onPasswordChanged = screen.onPasswordChanged
onLoginTapped = screen.onLoginTapped
}

@objc private func textDidChange(sender: UITextField) {
guard let text = sender.text else {
return
static func makeView(model: ObservableValue<LoginScreen>) -> some View {
VStack(spacing: 16) {
Text(model.title)

TextField(
"[email protected]",
text: model.binding(
get: \.email,
set: Action.emailUpdated
)
)
.autocapitalization(.none)
.autocorrectionDisabled()
.textContentType(.emailAddress)

SecureField(
"password",
text: model.binding(
get: \.password,
set: Action.passwordUpdated
),
onCommit: model.action(.login)
)

Button("Login", action: model.action(.login))
}
if sender == emailField {
onEmailChanged(text)
} else if sender == passwordField {
onPasswordChanged(text)
}
}

@objc private func buttonTapped(sender: UIButton) {
onLoginTapped()
.frame(maxWidth: 400)
}
}
16 changes: 3 additions & 13 deletions Samples/TicTacToe/Sources/Authentication/LoginWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,11 @@ extension LoginWorkflow {
typealias Rendering = LoginScreen

func render(state: LoginWorkflow.State, context: RenderContext<LoginWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)

return LoginScreen(
LoginScreen(
actionSink: context.makeSink(),
title: "Welcome! Please log in to play TicTacToe!",
email: state.email,
onEmailChanged: { email in
sink.send(.emailUpdated(email))
},
password: state.password,
onPasswordChanged: { password in
sink.send(.passwordUpdated(password))
},
onLoginTapped: {
sink.send(.login)
}
password: state.password
)
}
}
24 changes: 8 additions & 16 deletions Samples/TicTacToe/Tests/AuthenticationWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,10 @@ class AuthenticationWorkflowTests: XCTestCase {
.expectWorkflow(
type: LoginWorkflow.self,
producingRendering: LoginScreen(
actionSink: .noop(),
title: "",
email: "",
onEmailChanged: { _ in },
password: "",
onPasswordChanged: { _ in },
onLoginTapped: {}
password: ""
)
)
.render { screen in
Expand All @@ -210,12 +208,10 @@ class AuthenticationWorkflowTests: XCTestCase {
.expectWorkflow(
type: LoginWorkflow.self,
producingRendering: LoginScreen(
actionSink: .noop(),
title: "",
email: "",
onEmailChanged: { _ in },
password: "",
onPasswordChanged: { _ in },
onLoginTapped: {}
password: ""
)
)
.expect(
Expand Down Expand Up @@ -244,12 +240,10 @@ class AuthenticationWorkflowTests: XCTestCase {
.expectWorkflow(
type: LoginWorkflow.self,
producingRendering: LoginScreen(
actionSink: .noop(),
title: "",
email: "",
onEmailChanged: { _ in },
password: "",
onPasswordChanged: { _ in },
onLoginTapped: {}
password: ""
)
)
.expect(
Expand All @@ -275,12 +269,10 @@ class AuthenticationWorkflowTests: XCTestCase {
.expectWorkflow(
type: LoginWorkflow.self,
producingRendering: LoginScreen(
actionSink: .noop(),
title: "",
email: "",
onEmailChanged: { _ in },
password: "",
onPasswordChanged: { _ in },
onLoginTapped: {}
password: ""
)
)
.render { screen in
Expand Down
2 changes: 1 addition & 1 deletion Samples/Tutorial/AppHost/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import TutorialBase
import Tutorial5
import UIKit

@UIApplicationMain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,30 @@
* limitations under the License.
*/

import TutorialViews
import SwiftUI
import Workflow
import WorkflowUI
import WorkflowSwiftUI

struct TodoEditScreen: Screen {
struct TodoEditScreen: SwiftUIScreen, Equatable {
// The title of this todo item.
var title: String
// The contents, or "note" of the todo.
var note: String

// Callback for when the title or note changes
var onTitleChanged: (String) -> Void
var onNoteChanged: (String) -> Void

func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
return TodoEditViewController.description(for: self, environment: environment)
}
}

final class TodoEditViewController: ScreenViewController<TodoEditScreen> {
private var todoEditView: TodoEditView!

required init(screen: TodoEditScreen, environment: ViewEnvironment) {
super.init(screen: screen, environment: environment)
}

override func viewDidLoad() {
super.viewDidLoad()

todoEditView = TodoEditView(frame: view.bounds)
view.addSubview(todoEditView)

updateView(with: screen)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets)
}

override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) {
super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)

guard isViewLoaded else { return }

updateView(with: screen)
}

private func updateView(with screen: TodoEditScreen) {
// Update the view with the data from the screen.
todoEditView.title = screen.title
todoEditView.note = screen.note
todoEditView.onTitleChanged = screen.onTitleChanged
todoEditView.onNoteChanged = screen.onNoteChanged
var actionSink: ScreenActionSink<TodoEditWorkflow.Action>

static func makeView(model: ObservableValue<TodoEditScreen>) -> some View {
VStack {
TextField("Title", text: model.binding(
get: \.title,
set: Action.titleChanged
))
.font(.title)

TextEditor(text: model.binding(
get: \.note,
set: Action.noteChanged
))
}.padding()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,7 @@ extension TodoEditWorkflow {
let todoEditScreen = TodoEditScreen(
title: state.todo.title,
note: state.todo.note,
onTitleChanged: { title in
sink.send(.titleChanged(title))
},
onNoteChanged: { note in
sink.send(.noteChanged(note))
}
actionSink: .init(sink)
)

let backStackItem = BackStackScreen.Item(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class RootWorkflowTests: XCTestCase {
}

// Update the title.
editScreen.onTitleChanged("New Title")
editScreen.actionSink.send(.titleChanged("New Title"))
}

// Save the selected todo.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import BackStackContainer
import Workflow
import WorkflowSwiftUI
import WorkflowTesting
import WorkflowUI
import XCTest
Expand Down Expand Up @@ -78,8 +79,7 @@ class TodoWorkflowTests: XCTestCase {
screen: TodoEditScreen(
title: "Title",
note: "Note",
onTitleChanged: { _ in },
onNoteChanged: { _ in }
actionSink: .noop()
).asAnyScreen()
),
// Simulate it emitting an output of `.save` to update the state.
Expand Down
Loading