MacOS: conditional CommandGroup - swift

I have a custom CommandGroup (customPasteboardCommands - it replaces the built-in .pasteboard group) which I would like to render conditionally.
What is the correct syntax?
I tried two ways, none of them worked:
First
if viewModel.shouldUseCustomCommandGroup {
customPasteboardCommands
}
Error:
Closure containing control flow statement cannot be used with result
builder 'CommandsBuilder'
Second:
viewModel.shouldUseCustomCommandGroup ? customPasteboardCommands : EmptyCommands()
Error:
Result values in '? :' expression have mismatching types 'some
Commands' and 'EmptyCommands'
Full code:
App.swift
import SwiftUI
#main
struct macos_playgroundApp: App {
private let viewModel = ViewModel()
var body: some Scene {
WindowGroup {
Button("Toggle custom CommandGroup") {
viewModel.onToggleCustomCommandGroup()
}
}
.commands {
commands
}
}
#CommandsBuilder
var commands: some Commands {
emptyNewItemCommands
viewModel.shouldUseCustomCommandGroup ? customPasteboardCommands : EmptyCommands()
}
var emptyNewItemCommands: some Commands {
CommandGroup(replacing: .newItem) {}
}
var customPasteboardCommands: some Commands {
CommandGroup(replacing: .pasteboard) {
Button("Select All") {
print("Custom select all activated")
}
.keyboardShortcut("a", modifiers: [.command])
}
}
}
ViewModel.swift
import Foundation
class ViewModel: ObservableObject {
#Published var shouldUseCustomCommandGroup: Bool = false
func onToggleCustomCommandGroup() {
shouldUseCustomCommandGroup = !shouldUseCustomCommandGroup
}
}

Related

"Extra trailing closure passed in call" Error while passing multiple bindings

I am creating a simple Taskmanager for a schoolproject similar to apple's scrumdinger.
I created two filesafer for each struct. But if I try to pass them further I always get the error "Extra trailing closure passed in call". Can someone help me?
import SwiftUI
#main
struct Aufgaben_ManagerApp: App {
//#State private var tasks = MyTask.sampleData
//#State private var categories = Category.sampleData
#StateObject private var storeTask = TaskStore()
#StateObject private var storeCategory = CategoryStore()
#State private var errorWrapper: ErrorWrapper?
var body: some Scene {
WindowGroup {
NavigationView {
MainView(tasks: $storeTask.tasks, categories: $storeCategory.categories) {
Task {
do {
try await TaskStore.save(tasks: storeTask.tasks)
//try await CategoryStore.save(categories: storeCategory.categories)
} catch {
errorWrapper = ErrorWrapper(error: error, guidance: "Try again later.")
}
}
}
}
.task {
do {
storeTask.tasks = try await TaskStore.load()
// storeCategory.categories = try await CategoryStore.load()
} catch {
errorWrapper = ErrorWrapper(error: error, guidance: "TaskManager will load sample data and continue.")
}
}
.sheet(item: $errorWrapper, onDismiss: {
storeTask.tasks = MyTask.sampleData
// storeCategory.categories = Category.sampleData
}) { wrapper in
ErrorView(errorWrapper: wrapper)
}
}
}
}
It looks like the problem is further down the road. Maybe that the MainView cant accept the Bindings?
It looks like you have some logic that is outside the scope of the Task and Sheet modifiers. Specifically this code:
{ wrapper in
ErrorView(errorWrapper: wrapper)
}

Using Child SwitchStore Views within a Parent's ForEachStore View

I'm facing an issue implementing The Swift Composable Architecture in which I have a list of IdentifiedArray rows within my AppState that holds RowState which holds an EnumRowState as part of it's state, to allow me to SwitchStore on it within a RowView that is rendered within a ForEachStore on the AppView.
The problem I'm running into is that within a child reducer called liveReducer, there is an Effect.timer that is updating every one second, and should only be causing the LiveView to re-render.
However what's happening is that the AddView is also getting re-rendered every time too! The reason I know this is because I've manually added a Text("\(Date())") within the AddView and I see the date changing every one second regardless of the fact that it's not related in anyway to a change in state.
I've added .debug() to the appReducer and I see in the logs that the rows part of the sate shows ...(1 unchanged) which sounds right, so why then is every single row being re-rendered on every single Effect.timer effect?
Thank you in advance!
Below is how I've implemented this:
P.S. I've used a technique as describe here to pullback reducers on an enum property:
https://forums.swift.org/t/pullback-reducer-on-enum-property-switchstore/52628
Here is a video of the problem, in which you can see that once I've selected a name from the menu, ALL the views are getting updated, INCLUDING the navBarTitle!
https://youtube.com/shorts/rn_Yd57n1r8
struct AppState: Equatable {
var rows: IdentifiedArray<UUID, RowState> = []
}
enum AppAction: Equatable {
case row(id: UUID, action: RowAction)
}
public struct RowState: Equatable, Identifiable {
public var enumRowState: EnumRowState
public let id: UUID
}
public enum EnumRowState: Equatable {
case add(AddState)
case live(LiveState)
}
public enum RowAction: Equatable {
case live(AddAction)
case add(LiveAction)
}
public struct LiveState: Equatable {
public var secondsElapsed = 0
}
enum LiveAction: Equatable {
case onAppear
case onDisappear
case timerTicked
}
struct AppState: Equatable { }
enum AddAction: Equatable { }
public let liveReducer = Reducer<LiveState, LiveAction, LiveEnvironment>.init({
state, action, environment in
switch action {
case .onAppear:
return Effect
.timer(
id: state.baby.uid,
every: 1,
tolerance: .zero,
on: environment.mainQueue)
.map { _ in
LiveAction.timerTicked
})
case .timerTicked:
state.secondsElapsed += 1
return .none
case .onDisappear:
return .cancel(id: state.baby.uid)
}
})
public let addReducer = Reducer<AddState, AddAction, AddEnvironment>.init({
state, action, environment in
switch action {
})
}
///
/// Intermediate reducers to pull back to an Enum State which will be used within the `SwitchStore`
///
public var intermediateAddReducer: Reducer<EnumRowState, RowAction, RowEnvironment> {
return addReducer.pullback(
state: /EnumRowState.add,
action: /RowAction.add,
environment: { ... }
)
}
public var intermediateLiveReducer: Reducer<EnumRowState, RowAction, RowEnvironment > {
return liveReducer.pullback(
state: /EnumRowState.live,
action: /RowAction.live,
environment: { ... }
)
}
public let rowReducer: Reducer<RowState, RowAction, RowEnvironment> = .combine(
intermediateAddReducer.pullback(
state: \RowState.enumRowState,
action: /RowAction.self,
environment: { $0 }
),
intermediateLiveReducer.pullback(
state: \RowState.enumRowState,
action: /RowAction.self,
environment: { $0 }
)
)
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
rowReducer.forEach(
state: \.rows,
action: /AppAction.row(id:action:),
environment: { ... }
),
.init({ state, action, environment in
switch action {
case AppAction.onAppear:
state.rows = [
RowState(id: UUID(), enumRowState: .add(AddState()))
RowState(id: UUID(), enumRowState: .add(LiveState()))
]
return .none
default:
return .none
}
})
)
.debug()
public struct AppView: View {
let store: Store<AppState, AppAction>
public var body: some View {
WithViewStore(self.store) { viewStore in
List {
ForEachStore(
self.store.scope(
state: \AppState.rows,
action: AppAction.row(id:action:))
) { rowViewStore in
RowView(store: rowViewStore)
}
}
}
}
public struct RowView: View {
public let store: Store<RowState, RowAction>
public var body: some View {
WithViewStore(self.store) { viewStore in
SwitchStore(self.store.scope(state: \RowState.enumRowState)) {
CaseLet(state: /EnumRowState.add, action: RowAction.add) { store in
AddView(store: store)
}
CaseLet(state: /EnumRowState.live, action: RowAction.live) { store in
LiveView(store: store)
}
}
}
}
}
struct LiveView: View {
let store: Store<LiveState, LiveAction>
var body: some View {
WithViewStore(self.store) { viewStore in
Text(viewStore.secondsElapsed)
}
}
}
struct AddView: View {
let store: Store<AddState, AddAction>
var body: some View {
WithViewStore(self.store) { viewStore in
// This is getting re-rendered every time the `liveReducer`'s `secondsElapsed` state changes!?!
Text("\(Date())")
}
}
}

SwiftUI memory leak

I'm getting a weird memory leak in SwiftUI when using List and id: \.self, where only some of the items are destroyed. I'm using macOS Monterey Beta 5.
Here is how to reproduce:
Create a new blank SwiftUI macOS project
Paste the following code:
class Model: ObservableObject {
#Published var objs = (1..<100).map { TestObj(text: "\($0)")}
}
class TestObj: Hashable {
let text: String
static var numDestroyed = 0
init(text: String) {
self.text = text
}
static func == (lhs: TestObj, rhs: TestObj) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
deinit {
TestObj.numDestroyed += 1
print("Deinit: \(TestObj.numDestroyed)")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
List(model.objs, id: \.self) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
}
}
Run the app, and press the "Remove half" button. Keep pressing it until all the items are gone. However, if you look at the console, you'll see that only 85 items have been destroyed, while there were 99 items. The Xcode memory graph also supports this.
This seems to be caused by the id: \.self line. Removing it and switching it out for id: \.text fixes the problem.
However the reason I use id: \.self is because I want to support multiple selection, and I want the selection to be of type Set<TestObj>, instead of Set<UUID>.
Is there any way to solve this issue?
If you didn't have to use selection in your List, you could use any unique & constant id, for example:
class TestObj: Hashable, Identifiable {
let id = UUID()
/* ... */
}
And then your List with the implicit id: \.id:
List(model.objs) { obj in
Text(obj.text)
}
This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.
But you need selection in List, so see more below about how to achieve that.
To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.
Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:
extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}
Change your Model object to this:
class Model: ObservableObject {
#Published var objs: OrderedDictionary<UUID, TestObj>
init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}
And rather than model.objs you'll use model.objs.values, but that's it!
See full demo code below to test the selection:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selection: Set<UUID> = []
var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}
print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}
Result:

SwiftUI Error: "Closure containing control flow statement cannot be used with function builder 'ViewBuilder'"

I tried a SwiftUI tutorial, "Handling User Input".
https://developer.apple.com/tutorials/swiftui/handling-user-input
I tried implementing it with for instead of ForEach.
But an error arose: "Closure containing control flow statement cannot be used with function builder 'ViewBuilder'".
FROM:
import SwiftUI
struct LandmarkList: View {
#State var showFavoritesOnly = true
var body: some View {
NavigationView{
List{
Toggle(isOn: $showFavoritesOnly){
Text("Show FavatiteOnly")
}
ForEach(landmarkData) { landmark in
if !self.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
TO (I wrote):
import SwiftUI
struct LandmarkList: View {
#State var showFavoritesOnly = true
var body: some View {
NavigationView{
List{
Toggle(isOn: $showFavoritesOnly){
Text("Show FavatiteOnly")
}
for landmark in landmarkData {
if $showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)){
LandmarkRow(landmark: landmark)}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
The ForEach confirms to View, so at its core, it is a View just like a TextField. ForEach Relationships
You can't use a normal for-in because the ViewBuilder doesn't understand what is an imperative for-loop. The ViewBuilder can understand other control flow like if, if-else or if let using buildEither(first:), buildEither(second:), and buildif(_:) respectively.
Try to comment out the if statement, and it might reveal you the real error. Here's one example: I was getting this error because of a missing associated value of an enum passed to one of my views.
Here's how it looked like before and after:
Before
Group {
if let value = valueOrNil {
FooView(
bar: [
.baz(arg1: 0, arg2: 3)
]
)
}
}
After
Group {
if let value = valueOrNil {
FooView(
bar: [
.baz(arg1: 0, arg2: 3, arg3: 6)
]
)
}
}

How to modify a user input inside a SwiftUI form loop

I'm developing a simple SwiftUI app in Xcode 11. I want to have a form that loops through multiple user input strings and displays a form with a button. When the user presses the button it modifies the input value - specifically increment or decrement it.
However when passing an array of references like UserInput().foo where UserInput is a published observable object I cannot modify the value inside a ForEach because the ForEach is passed a copy as oppose to the original reference (at least that's my basic understanding). How do I then try to achieve it? I read about inout and everybody says to avoid it but surely this must be a relatively common issue.
I've made an simple example of what I'm trying to do but I can't quite work it out:
import SwiftUI
class UserInput: ObservableObject {
#Published var foo: String = ""
#Published var bar: String = ""
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView?{
var userinputs = [
[UserInput().foo, "Foo"],
[UserInput().bar, "Bar"]
]
var inputs: some View{
VStack(){
ForEach(userinputs, id: \.self){userinput in
Text("\(userinput[1]): \(String(userinput[0]))")
Button(action: {
increment(input: String(userinput[0]))
}){
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String){
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
As I understood, when adding a value to userinputs, the ForEach values doesn't change.
Well, if that's the case, first of all, you could try creating a struct and in it, you declare foo and bar, then just declare a variable of type the struct. It'll look like this:
struct Input: Identifiable {
var id = UUID()
var foo: String
var bar: String
}
class UserInput: ObservableObject {
#Published var inputs: [Input] = [Input]()
}
//ContentView
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView? {
var inputs: some View {
VStack {
ForEach(input.inputs) { userinput in
Text("\(userinput.bar): \(String(userinput.foo))")
Button(action: {
increment(input: String(userinput.foo))
}) {
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String) {
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
Wouldn't this be easier and more elegant?