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

Replace full screen state view by in-row state view while running wat… #2787

Merged
merged 2 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@
421B1C1D2BD65C04001ED18C /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */; };
42266B112B740E4C00E94A71 /* BarcodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */; };
42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */; };
423E95C52BFF2F5E00C2094A /* WatchActionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 423E95C32BFF2ED000C2094A /* WatchActionButtonView.swift */; };
423E95C92BFF314F00C2094A /* MockWatchHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 423E95C72BFF314C00C2094A /* MockWatchHomeViewModel.swift */; };
424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; };
424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; };
424DD05A2B3509170057E456 /* CarPlayActionsTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424DD0592B3509170057E456 /* CarPlayActionsTemplate.swift */; };
Expand Down Expand Up @@ -1647,6 +1649,8 @@
421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = "<group>"; };
42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerView.swift; sourceTree = "<group>"; };
42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerViewModel.swift; sourceTree = "<group>"; };
423E95C32BFF2ED000C2094A /* WatchActionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchActionButtonView.swift; sourceTree = "<group>"; };
423E95C72BFF314C00C2094A /* MockWatchHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWatchHomeViewModel.swift; sourceTree = "<group>"; };
4242A2B12B2B5C8000E9F001 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/AppIntentVocabulary.plist; sourceTree = "<group>"; };
4242A2B22B2B5C8100E9F001 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "ca-ES"; path = "ca-ES.lproj/AppIntentVocabulary.plist"; sourceTree = "<group>"; };
4242A2B32B2B5C8100E9F001 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/AppIntentVocabulary.plist"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3198,6 +3202,22 @@
path = Widgets;
sourceTree = "<group>";
};
423E95C22BFF2EC500C2094A /* Views */ = {
isa = PBXGroup;
children = (
423E95C32BFF2ED000C2094A /* WatchActionButtonView.swift */,
);
path = Views;
sourceTree = "<group>";
};
423E95C62BFF314200C2094A /* Mocks */ = {
isa = PBXGroup;
children = (
423E95C72BFF314C00C2094A /* MockWatchHomeViewModel.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
425573C52B55729E00145217 /* Servers */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3352,6 +3372,8 @@
42C373B62BC55C3A00898990 /* Home */ = {
isa = PBXGroup;
children = (
423E95C62BFF314200C2094A /* Mocks */,
423E95C22BFF2EC500C2094A /* Views */,
42C373B02BC536AA00898990 /* WatchHomeView.swift */,
42C373B42BC53B1400898990 /* WatchHomeViewModel.swift */,
);
Expand Down Expand Up @@ -6408,7 +6430,9 @@
B6CC5D962159D10E00833E5D /* ExtensionDelegate.swift in Sources */,
110D74CA2640E0DF00406078 /* NotificationSubControllerMedia.swift in Sources */,
B672AB582216B5E000175465 /* Date+ComplicationDivination.swift in Sources */,
423E95C92BFF314F00C2094A /* MockWatchHomeViewModel.swift in Sources */,
42C373B22BC5382900898990 /* HostingController.swift in Sources */,
423E95C52BFF2F5E00C2094A /* WatchActionButtonView.swift in Sources */,
B6CC5D982159D10E00833E5D /* ComplicationController.swift in Sources */,
11169B7C262BDE80005EF90A /* DynamicNotificationController.swift in Sources */,
11684B7A263F994600B48EC3 /* NotificationSubControllerMJPEG.swift in Sources */,
Expand Down
25 changes: 25 additions & 0 deletions Sources/Extensions/Watch/Home/Mocks/MockWatchHomeViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if DEBUG
import Foundation

final class MockWatchHomeViewModel: WatchHomeViewModelProtocol {
@Published var actions: [WatchActionItem] = []
@Published var state: WatchHomeViewState = .idle

func runActionId(_ actionId: String, completion: @escaping (Bool) -> Void) {}

func onAppear() {
actions = [
.init(
id: "1",
name: "Hello",
iconName: "ab_testing",
backgroundColor: "#34eba8",
iconColor: "#4479b3",
textColor: "#4479b3"
),
]
}

func onDisappear() {}
}
#endif
93 changes: 93 additions & 0 deletions Sources/Extensions/Watch/Home/Views/WatchActionButtonView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Shared
import SwiftUI

struct WatchActionButtonView<ViewModel>: View where ViewModel: WatchHomeViewModelProtocol {
enum ActionState {
case idle
case loading
case success
case failure
}

@EnvironmentObject private var homeViewModel: ViewModel

let action: WatchActionItem

@State private var state: ActionState = .idle

var body: some View {
content
.onChange(of: state) { newValue in
// On watchOS 10 this can be replaced by '.sensoryFeedback' modifier
let currentDevice = WKInterfaceDevice.current()
switch newValue {
case .success:
currentDevice.play(.success)
case .failure:
currentDevice.play(.failure)
case .loading:
currentDevice.play(.click)
default:
break
}
}
}

@ViewBuilder
private var content: some View {
Button {
state = .loading
homeViewModel.runActionId(action.id) { success in
state = success ? .success : .failure
resetState()
}
} label: {
HStack(spacing: Spaces.one) {
iconToDisplay
.animation(.easeInOut, value: state)
Text(action.name)
.foregroundStyle(Color(uiColor: .init(hex: action.textColor)))
}
}
.disabled(state != .idle)
.listRowBackground(
Color(uiColor: .init(hex: action.backgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 12))
)
}

private func resetState() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
state = .idle
}
}

private var iconToDisplay: some View {
VStack {
switch state {
case .idle:
Image(uiImage: MaterialDesignIcons(named: action.iconName).image(
ofSize: .init(width: 24, height: 24),
color: .init(hex: action.iconColor)
))
case .loading:
ProgressView()
.progressViewStyle(.circular)
.frame(width: 24, height: 24)
.tint(.black)
.shadow(color: .white, radius: 10)
case .success:
makeActionImage(iconName: MaterialDesignIcons.checkIcon.name)
case .failure:
makeActionImage(iconName: MaterialDesignIcons.closeThickIcon.name)
}
}
}

private func makeActionImage(iconName: String) -> some View {
Image(uiImage: MaterialDesignIcons(named: iconName).image(
ofSize: .init(width: 24, height: 24),
color: .init(hex: action.iconColor)
))
}
}
83 changes: 2 additions & 81 deletions Sources/Extensions/Watch/Home/WatchHomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ struct WatchHomeView<ViewModel>: View where ViewModel: WatchHomeViewModelProtoco
ZStack {
list
noActionsView
stateView
}
.onAppear {
viewModel.onAppear()
Expand All @@ -28,32 +27,6 @@ struct WatchHomeView<ViewModel>: View where ViewModel: WatchHomeViewModelProtoco
}
}

@ViewBuilder
private var stateView: some View {
VStack {
switch viewModel.state {
case .loading:
ProgressView()
.progressViewStyle(.circular)
case .success:
Image(uiImage: MaterialDesignIcons.checkCircleIcon.image(ofSize: stateIconSize, color: stateIconColor))
.onAppear {
interfaceDevice.play(.success)
}
case .failure:
Image(uiImage: MaterialDesignIcons.closeIcon.image(ofSize: stateIconSize, color: stateIconColor))
.onAppear {
interfaceDevice.play(.failure)
}
case .idle:
EmptyView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(stateViewBackground)
.opacity(viewModel.state != .idle ? 1 : 0)
}

private var stateViewBackground: some ShapeStyle {
if #available(watchOS 10, *) {
return .regularMaterial
Expand All @@ -64,22 +37,8 @@ struct WatchHomeView<ViewModel>: View where ViewModel: WatchHomeViewModelProtoco

private var list: some View {
List(viewModel.actions, id: \.id) { action in
Button {
viewModel.runActionId(action.id)
} label: {
HStack(spacing: Spaces.one) {
Image(uiImage: MaterialDesignIcons(named: action.iconName).image(
ofSize: .init(width: 24, height: 24),
color: .init(hex: action.iconColor)
))
Text(action.name)
.foregroundStyle(Color(uiColor: .init(hex: action.textColor)))
}
}
.listRowBackground(
Color(uiColor: .init(hex: action.backgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 12))
)
WatchActionButtonView<ViewModel>(action: action)
.environmentObject(viewModel)
}
.animation(.easeInOut, value: viewModel.actions)
}
Expand All @@ -96,42 +55,4 @@ struct WatchHomeView<ViewModel>: View where ViewModel: WatchHomeViewModelProtoco
#Preview {
WatchHomeView(viewModel: MockWatchHomeViewModel())
}

final class MockWatchHomeViewModel: WatchHomeViewModelProtocol {
func runActionId(_ actionId: String) {
DispatchQueue.main.async {
self.state = .loading
}

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .success
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.state = .failure
}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.state = .idle
}
}

@Published var actions: [WatchActionItem] = []
@Published var state: WatchHomeViewState = .idle

func onAppear() {
actions = [
.init(
id: "1",
name: "Hello",
iconName: "ab_testing",
backgroundColor: "#34eba8",
iconColor: "#4479b3",
textColor: "#4479b3"
),
]
}

func onDisappear() {}
}
#endif
42 changes: 10 additions & 32 deletions Sources/Extensions/Watch/Home/WatchHomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ struct WatchActionItem: Equatable {

protocol WatchHomeViewModelProtocol: ObservableObject {
var actions: [WatchActionItem] { get set }
var state: WatchHomeViewState { get set }
func onAppear()
func onDisappear()
func runActionId(_ actionId: String)
func runActionId(_ actionId: String, completion: @escaping (Bool) -> Void)
}

enum WatchHomeViewState {
Expand All @@ -35,11 +34,6 @@ final class WatchHomeViewModel: WatchHomeViewModelProtocol {
}

@Published var actions: [WatchActionItem] = []
@Published var state: WatchHomeViewState = .idle {
didSet {
resetStateToIdleIfNeeded()
}
}

private var actionsToken: NotificationToken?
private var realmActions: [Action] = []
Expand All @@ -52,13 +46,14 @@ final class WatchHomeViewModel: WatchHomeViewModelProtocol {
actionsToken?.invalidate()
}

func runActionId(_ actionId: String) {
guard let selectedAction = realmActions.first(where: { $0.ID == actionId }) else { return }
func runActionId(_ actionId: String, completion: @escaping (Bool) -> Void) {
guard let selectedAction = realmActions.first(where: { $0.ID == actionId }) else {
completion(false)
return
}

Current.Log.verbose("Selected action id: \(actionId)")

setState(.loading)

firstly { () -> Promise<Void> in
Promise { seal in
guard Communicator.shared.currentReachability == .immediatelyReachable else {
Expand Down Expand Up @@ -93,17 +88,11 @@ final class WatchHomeViewModel: WatchHomeViewModelProtocol {

Current.Log.error("recovering error \(error) by trying locally")
return Current.api(for: server).HandleAction(actionID: selectedAction.ID, source: .Watch)
}.done { [weak self] in
self?.setState(.success)
}.catch { [weak self] err in
}.done {
completion(true)
}.catch { err in
Current.Log.error("Error during action event fire: \(err)")
self?.setState(.failure)
}
}

private func setState(_ state: WatchHomeViewState) {
DispatchQueue.main.async { [weak self] in
self?.state = state
completion(false)
}
}

Expand All @@ -129,17 +118,6 @@ final class WatchHomeViewModel: WatchHomeViewModelProtocol {
}
}
}

private func resetStateToIdleIfNeeded() {
switch state {
case .success, .failure:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.state = .idle
}
default:
break
}
}
}

private extension Action {
Expand Down
Loading