SwiftUI platform testing deuglification (composing view hierarchies) - swiftui

I could use a way or two to make this not as ugly moving the filler for HStack/List into a common helper method or make a helper for wrapper of the ForEach to prebuild a portion of a view hierarchy somehow?
var body: some View {
NavigationView {
if horizontalScroll {
ScrollView (.horizontal, showsIndicators: false) {
HStack {
ForEach(videos) { video in
Button {
selectedVideo = video
} label: {
VideoRow(video: video)
}
}
}
}
} else {
List {
// makeEmbeddedVideoPlayer()
ForEach(videos) { video in
Button {
selectedVideo = video
} label: {
VideoRow(video: video)
}
}
}
.navigationTitle("Travel Vlogs")
}
}
.fullScreenCover(item: $selectedVideo) {
embeddedVideoRate = 1.0
} content: { item in
makeFullScreenVideoPlayer(for: item)
}
}
var horizontalScroll: Bool {
#if os(tvOS)
return true
#else
return false
#endif
}
A naive attempt to make a common helper yielded this
What's the return type for the guts of List/Hstack?

Related

Monitor for Keypress in Meun/Picker in SwiftUI on MacOS

I'm learning SwiftUI programming by trying to duplicate basic features in the MacOS Finder.
The groups button in the Finder window (screenshot below) has me stumped. Clicking the menu shows the group options, while option-clicking shows the sort options. I can't figure out how that's done.
My basic code is as follows:
Menu {
if NSEvent.modifierFlags.contains(.option) {
Picker(selection: viewSorts, label: EmptyView()) {
ForEach(viewSorts) { sort in
Text(sort.name).tag(sort)
}
}
.labelsHidden()
.pickerStyle(InlinePickerStyle())
} else {
Picker(selection: viewGroups, label: EmptyView()) {
ForEach(viewGroups) { group in
Text(group.name).tag(group)
}
.labelsHidden()
.pickerStyle(InlinePickerStyle())
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
It works, however NSEvent.modifierFlags.contains(.option) never evaluates to true.
This post has two examples that I used to try to fix the problem:
Using onTapGesture with EventModifiers:
#State private var showSort = false
Menu {
if showSort {
// ... show sort picker ...
} else {
// ... show group picker ...
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
.gesture(TapGesture().modifiers(.option).onEnded {
showSort = true
})
.onTapGesture {
showSort = false
}
And another using a CGKeyCode extension:
import CoreGraphics
extension CGKeyCode
{
static let kVK_Option : CGKeyCode = 0x3A
static let kVK_RightOption: CGKeyCode = 0x3D
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
static var optionKeyPressed: Bool {
return Self.kVK_Option.isPressed || Self.kVK_RightOption.isPressed
}
}
Menu {
if CGKeyCode.optionIsPressed {
// ... show sort picker ...
} else {
// ... show group picker ...
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
And from these two posts (1, 2), addLocalMonitorForEvents:
#State private var showSort = false
Menu {
if showSort {
// ... show sort picker ...
} else {
// ... show group picker ...
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
.onAppear() {
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (keyEvent) -> NSEvent? in
if keyEvent.modifierFlags == .option {
showSort = true
} else {
showSort = false
}
return keyEvent
}
}
The answer is probably staring at me in the face, but I just can't see it! Thank you for any help!
UPDATE: onContinuousHover does work, but it only updates when the mouse is moving over the menu.
.onContinuousHover { _ in
showSort = NSEvent.modifierFlags.contains(.option) ? true : false
}
But onTapGesture doesn't work
.onTapGesture {
showSort = NSEvent.modifierFlags.contains(.option) ? true : false
}

How can I remove the #available tag in the this Swift struct?

I have the following code in my WidgetBundle class, but I want to remove the #available tag since as is, if the user is not iOS 16, they won't have access to Home Screen or Lock Screen widgets at all.
struct WidgetBundler: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
HomeScreenWidget()
LockScreenWidget()
}
}
The compiler does not like this code, and it was the only other way I could think of to get around this issue:
struct WidgetBundler: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
if #available(iOSApplicationExtension 16, *) {
HomeScreenWidget()
LockScreenWidget()
} else {
HomeScreenWidget()
}
}
}
How do I remove the #available tag and get it so users can access the Home Screen widgets even on iOS 14 and 15?
This solution works:
#main
struct WidgetsBudle: WidgetBundle {
var body: some Widget {
if #available(iOSApplicationExtension 16.0, *) {
return ios16Widgets
} else {
return ios14Widgets
}
}
#WidgetBundleBuilder
var ios14Widgets: some Widget {
HomeScreenWidget()
}
#WidgetBundleBuilder
var ios16Widgets: some Widget {
HomeScreenWidget()
LockScreenWidget()
}
}
You can use this approach mentioned in avanderlee
#main
struct WidgetsBudler: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
widgets()
}
func widgets() -> some Widget {
if #available(iOS 16.0, *) {
return WidgetBundleBuilder.buildBlock(HomeScreenWidget(),
LockScreenWidget())
} else {
return HomeScreenWidget()
}
}
}

Display a View when rest of content is empty

I have a view body with logic such as this:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
This works fine to conditionally show elements vertically stacked. However, if none of the conditions are satisfied, the VStack is empty and my UI looks broken. I would like to show a placeholder instead. My current solution is to add a manual check at the end on !someCondition && !anotherCondition && !thirdCondition:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
if !someCondition && !anotherCondition && !thirdCondition { // 👈
Text("Please select an element.")
}
}
}
However, this is difficult to keep the condition in sync with the content above. I was hoping there was some sort of view modifier I could use such as:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}.emptyState { // 👈
Text("Please select an element.")
}
}
The closest thing I could find is this tutorial, but that requires passing in the condition as well.
Is there a way to build a view modifier like this emptyState which doesn't require duplicating the condition logic?
I was thinking I could use a ZStack for this:
var body: some View {
ZStack { // 👈
// empty state text
Text("Please select an element.")
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
}
... but then I run into a different issue where if I'm showing real content (e.g. SomeView()) but it's not large enough, I could see both SomeView() and the empty state text.
Here's one implementation using GeometryReader & it's named emptyState:
extension View {
func emptyState<Content: View>(#ViewBuilder content: () -> Content) -> some View {
return self.modifier(EmptyStateModifier(placeHolder: content()))
}
}
struct EmptyStateModifier<PlaceHolder: View>: ViewModifier {
#State var isEmpty = false
let placeHolder: PlaceHolder
func body(content: Content) -> some View {
ZStack {
if isEmpty {//Thanks to #Asperi
placeHolder
}
content
.background(
GeometryReader { reader in
Color.clear
.onChange(of: reader.frame(in: .global).size == .zero) { newValue in
isEmpty = reader.frame(in: .global).size == .zero
}
}
)
}
}
}
If it is a long chaining condition, you can handle it with switch{}, then use the benefit of default to display the placeholder when 0 condition is met(stack is empty or no selection)
#State var selected = ""
var body: some View {
VStack {
switch selected {
case "a":
SomeView()
case "b":
AnotherView()
case "c":
ThirdView()
//this default will show up
//when there is no selection
//and when the stack is empty meaning that all the above
//conditions did not meet
default:
Text("Please select an element")
}
}
}
There is no straightforward, officially supported way of telling what the return value of a view builder contains.
It is more sensible to handle this at the model layer than in your view. Each of your conditions are part of your model. These should be wrapped up into a single value type, and you can use the presence or absence of that (or an internal calculated value of that, depending on your requirements) to inform what to put in the stack. For example:
struct Model {
let one: Bool
let two: Bool
let three: Bool
}
struct MyView: View {
let model: Model?
var body: some View {
VStack {
switch model {
case .some(let model):
if model.one {
Text("One")
}
if model.two {
Text("Two")
}
if model.three {
Text("Three")
}
case .none:
Text("Empty")
}
}
}
}
(I'm assuming here that there is no valid Model which doesn't contain any of the values, that would be when it is set to nil)

SwiftUI: NavigationLink doesn't show animation if setting after background task

I am trying to use the new async/await concurrency code with a SwiftUI view to do some basic asynchronous data loading after a button click, to show a spinner view, and on completion, transition to a new page. I use an enum to mark the state of the view, and use that with NavigationLink to move forward to a new view. This works, and shows the 'loading' text view for a second before 'pushing' to the next view, but there is no animation when the new view is pushed. This is the code I am using:
import SwiftUI
enum ContinueActionState: Int {
case ready = 0
case showProgressView = 1
case pushToNextPage = 2
case readingError = 3
case error = 4
}
struct ContentView: View {
#State var actionState: ContinueActionState? = .ready
var body: some View {
NavigationView {
VStack {
Text("Test the Button!")
if (actionState == .showProgressView) {
ProgressView("Loading Data")
} else if (actionState == .error || actionState == .readingError) {
Text("Error in loading something")
}
else {
NavigationLink(destination: DetailPageView(), tag: .pushToNextPage, selection: $actionState) {
Button(action: {
Task {
print("on buttonClick isMain =\(Thread.isMainThread)")
self.actionState = .showProgressView
await self.startProcessingData()
//self.actionState = .pushToNextPage // animation works if only this is used
}
}) {
Text("Continue")
}
.tint(.blue)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 5))
.controlSize(.large)
}
}
}.navigationTitle("Test Async")
}
}
func startProcessingData() async {
Task.detached {
print("startProcessingData isMain =\(Thread.isMainThread)")
try await Task.sleep(nanoseconds: 1_000_000_000)
//await MainActor.run {
self.actionState = .pushToNextPage
//}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailPageView: View {
var body: some View {
Text("Detail page")
}
}
IF I just forego the async call and just set to .pushToNextPage state immediately on button click, the animation works fine.
Is there any way to get this to work with a smooth animation, after processing stuff in a background queue task is complete?
if you move the NavigationLink out of the if statements it works fine for me:
NavigationView {
VStack {
Text("Test the Button!")
NavigationLink(destination: DetailPageView(), tag: .pushToNextPage, selection: $actionState) { Text("") } // here
if (actionState == .showProgressView) {
ProgressView("Loading Data")
} else if (actionState == .error || actionState == .readingError) {
Text("Error in loading something")
}
else {
Button(action: {
Task {
print("on buttonClick isMain =\(Thread.isMainThread)")
self.actionState = .showProgressView
await self.startProcessingData()
}
}) {
Text("Continue")
}
.tint(.blue)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 5))
.controlSize(.large)
}
}.navigationTitle("Test Async")
}

Confirm from model in SwiftUI

let us imagine that I have something like the following core/model:
class Core: ObservableObject {
...
func action(confirm: () -> Bool) {
if state == .needsConfirmation, !confirm() {
return
}
changeState()
}
...
}
and then I use this core object in a SwiftUI view.
struct ListView: View {
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
core.action {
// present an alert to the user and return if the user confirms or not
}
}
}
}
}
So boiling it down, I wonder how to work with handlers there need an input from the user, and I cant wrap my head around it.
It looks like you reversed interactivity concept, instead you need something like below (scratchy)
struct ListView: View {
#State private var confirmAlert = false
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
if core.needsConfirmation {
self.confirmAlert = true
} else {
self.core.action() // << direct action
}
}
}
.alert(isPresented: $confirmAlert) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .default(Text("Confirm")) {
self.core.needsConfirmation = false
self.core.action() // <<< confirmed action
},
secondaryButton: .cancel())
}
}
}
class Core: ObservableObject {
var needsConfirmation = true
...
func action() {
// just act
}
...
}
Alternate: with hidden condition checking in Core
struct ListView: View {
#ObservedObject core: Core
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
self.core.action() // << direct action
}
}
.alert(isPresented: $core.needsConfirmation) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .default(Text("Confirm")) {
self.core.action(state: .confirmed) // <<< confirmed action
},
secondaryButton: .cancel())
}
}
}
class Core: ObservableObject {
#Published var needsConfirmation = false
...
func action(state: State = .check) {
if state == .check && self.state != .confirmed {
self.needsConfirmation = true
return;
}
self.state = state
// just act
}
...
}