Skip to content

[DNM WIP] SwiftUI testbed app #240

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

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ccbec00
paste in WorkflowSwiftUI types
square-tomb May 25, 2023
af1499d
try adding dependency on MarketWorkflowUI
square-tomb Aug 10, 2023
159fb94
specify Market version compatible w/ Workflow 3.0
square-tomb Aug 10, 2023
b8d48b1
add SwiftUITestbed sample app
square-tomb Aug 10, 2023
74d3a5f
allow pushing screen onto backstack
square-tomb Aug 11, 2023
41c5de1
allow presenting a screen modally
square-tomb Aug 11, 2023
2e2cbfe
separate RootWorkflow from MainWorkflow
square-tomb Aug 11, 2023
90defdb
add editable title
square-tomb Aug 11, 2023
0f1642a
push another MainScreen instead of a placeholder
square-tomb Aug 11, 2023
76483f3
show Close button when modal can be dismissed
square-tomb Aug 14, 2023
b31d73c
add Caps toggle
square-tomb Aug 15, 2023
33d88b1
disable toggle when title empty
square-tomb Aug 15, 2023
3ceb958
use FocusState
square-tomb Aug 15, 2023
133088f
move source files to correct location
square-tomb Aug 16, 2023
52f9c04
add tests
square-tomb Aug 17, 2023
e2bb48c
Revert "paste in WorkflowSwiftUI types"
square-tomb Aug 25, 2023
6778afb
organize UI
square-tomb Aug 25, 2023
e91728f
remove PlaceholderScreen
square-tomb Aug 25, 2023
05a14ef
Update Samples/SwiftUITestbed/Sources/AppDelegate.swift
square-tomb Sep 27, 2023
92b6a5e
use preferred modal presentation syntax
square-tomb Sep 27, 2023
e116cfa
add Resign Focus button
square-tomb Sep 27, 2023
1857f0c
revert change to Gemfile.lock
square-tomb Sep 27, 2023
cee500b
update Market dependency
square-tomb Sep 28, 2023
4942daa
add SwiftUI Preview
square-tomb Sep 28, 2023
11ff615
Abstract ToggleRow component
n8chur Sep 28, 2023
8200320
wrap preview in #if DEBUG
square-tomb Oct 4, 2023
61e8bb7
remove unneeded external_source_pods declaration
square-tomb Oct 4, 2023
b63c2d4
Merge branch 'main' of github.com:square/workflow-swift into tomb/swi…
square-tomb Nov 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gen_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ platforms:
- ios
podspec_paths:
- Development.podspec

external_source_pods:
- MarketWorkflowUI:
- :git: 'https://github.com/squareup/market.git'
:tag: 'ios/MarketWorkflowUI/80.0.0'
13 changes: 13 additions & 0 deletions Development.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ Pod::Spec.new do |s|
test_spec.source_files = 'Samples/TicTacToe/Tests/**/*.swift'
end

s.app_spec 'SwiftUITestbed' do |app_spec|
app_spec.source_files = 'Samples/SwiftUITestbed/Sources/**/*.swift'
app_spec.dependency 'MarketWorkflowUI', '80.0.0'
end

s.test_spec 'SwiftUITestbedTests' do |test_spec|
test_spec.dependency 'Development/SwiftUITestbed'
test_spec.dependency 'WorkflowTesting'
test_spec.requires_app_host = true
test_spec.app_host_name = 'Development/SwiftUITestbed'
test_spec.source_files = 'Samples/SwiftUITestbed/Tests/**/*.swift'
end

s.app_spec 'SampleSplitScreen' do |app_spec|
app_spec.dependency 'SplitScreenContainer'
app_spec.source_files = 'Samples/SplitScreenContainer/DemoApp/**/*.swift'
Expand Down
45 changes: 45 additions & 0 deletions Samples/SwiftUITestbed/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2023 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import MarketWorkflowUI
import UIKit
import WorkflowUI

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = WorkflowHostingController(
workflow: RootWorkflow(close: nil)
.mapRendering(ModalHostContainer.init)
.mapRendering(MarketRootScreen.init)
)
window?.makeKeyAndVisible()
return true
}

func applicationWillResignActive(_ application: UIApplication) {}

func applicationDidEnterBackground(_ application: UIApplication) {}

func applicationWillEnterForeground(_ application: UIApplication) {}

func applicationDidBecomeActive(_ application: UIApplication) {}

func applicationWillTerminate(_ application: UIApplication) {}
}
140 changes: 140 additions & 0 deletions Samples/SwiftUITestbed/Sources/MainScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 2023 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import BlueprintUI
import MarketUI
import MarketWorkflowUI
import SwiftUI
import ViewEnvironment

struct MainScreen: MarketScreen {
enum Field: Hashable {
case title
}

@BlueprintUI.FocusState var focusedField: Field?

let title: String
let didChangeTitle: (String) -> Void

let allCapsToggleIsOn: Bool
let allCapsToggleIsEnabled: Bool
let didChangeAllCapsToggle: (Bool) -> Void

let didTapPushScreen: () -> Void
let didTapPresentScreen: () -> Void

let didTapClose: (() -> Void)?

func element(
in context: MarketWorkflowUI.MarketScreenContext,
with styles: MarketTheming.MarketStylesheet
) -> BlueprintUI.Element {
MarketScreenContent {
Column(
underflow: .justifyToStart,
minimumSpacing: styles.spacings.spacing200
) {
MarketInlineSectionHeader(
style: styles.headers.inlineSection20,
title: "Title"
)

MarketTextField(
style: styles.fields.textField,
label: "Text",
text: title,
onChange: didChangeTitle,
onReturn: { _ in focusedField = nil }
)
.focused(when: $focusedField, equals: .title)
.onAppear { focusedField = .title }

Row(
alignment: .center,
minimumSpacing: styles.spacings.spacing200
) {
MarketLabel(
textStyle: styles.typography.semibold20,
color: styles.colors.text10,
text: "All Caps"
)

MarketToggle(
style: styles.toggle.normal,
isOn: allCapsToggleIsOn,
isEnabled: allCapsToggleIsEnabled,
accessibilityLabel: "is all caps",
onChange: didChangeAllCapsToggle
)
}

Spacer(styles.spacings.spacing50)

MarketInlineSectionHeader(
style: styles.headers.inlineSection20,
title: "Navigation"
)

MarketButton(
style: styles.button(rank: .secondary),
text: "Push Screen",
onTap: didTapPushScreen
)

MarketButton(
style: styles.button(rank: .secondary),
text: "Present Screen",
onTap: didTapPresentScreen
)

MarketButton(
style: styles.button(rank: .secondary),
text: "Resign Focus",
onTap: { focusedField = nil }
)
}
}
}
}

extension MainScreen: MarketBackStackContentScreen {
func backStackItem(in environment: ViewEnvironment) -> MarketUI.MarketNavigationItem {
MarketNavigationItem(
title: .text(.init(regular: title)),
backButton: didTapClose.map { .close(onTap: $0) } ?? .automatic()
)
}

var backStackIdentifier: AnyHashable? { nil }
}

struct MainScreen_Preview: PreviewProvider {
static var previews: some View {
MainScreen(
title: "New item",
didChangeTitle: { _ in },
allCapsToggleIsOn: true,
allCapsToggleIsEnabled: true,
didChangeAllCapsToggle: { _ in },
didTapPushScreen: {},
didTapPresentScreen: {},
didTapClose: {}
)
.asMarketBackStack()
.marketPreview()
}
}
91 changes: 91 additions & 0 deletions Samples/SwiftUITestbed/Sources/MainWorkflow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2023 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import MarketWorkflowUI
import Workflow

struct MainWorkflow: Workflow {
let didClose: (() -> Void)?

enum Output {
case pushScreen
case presentScreen
}

struct State {
var title: String
var isAllCaps: Bool

init(title: String) {
self.title = title
self.isAllCaps = title.isAllCaps
}
}

func makeInitialState() -> State {
State(title: "New item")
}

enum Action: WorkflowAction {
typealias WorkflowType = MainWorkflow

case pushScreen
case presentScreen
case changeTitle(String)
case changeAllCaps(Bool)

func apply(toState state: inout WorkflowType.State) -> WorkflowType.Output? {
switch self {
case .pushScreen:
return .pushScreen
case .presentScreen:
return .presentScreen
case .changeTitle(let newValue):
state.title = newValue
state.isAllCaps = newValue.isAllCaps
case .changeAllCaps(let isAllCaps):
state.isAllCaps = isAllCaps
state.title = isAllCaps ? state.title.uppercased() : state.title.lowercased()
}
return nil
}
}

typealias Rendering = MainScreen

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

return MainScreen(
title: state.title,
didChangeTitle: { sink.send(.changeTitle($0)) },
allCapsToggleIsOn: state.isAllCaps,
allCapsToggleIsEnabled: !state.title.isEmpty,
didChangeAllCapsToggle: { sink.send(.changeAllCaps($0)) },
didTapPushScreen: { sink.send(.pushScreen) },
didTapPresentScreen: { sink.send(.presentScreen) },
didTapClose: didClose
)
}
}

private extension String {
var isAllCaps: Bool {
allSatisfy { character in
character.isUppercase || !character.isCased
}
}
}
Loading