I'm using NavigationSplitView to structure the user interface of my (macOS) app like this:
struct NavigationView: View {
#State private var selectedApplication: Application?
var body: some View {
NavigationSplitView {
ApplicationsView(selectedApplication: $selectedApplication)
} detail: {
Text(selectedApplication?.name ?? "Nothing selected")
}
}
}
The sidebar is implemented using ApplicationsView that looks like this:
struct ApplicationsView: View {
#FetchRequest(fetchRequest: Application.all()) private var applications
#Binding var selectedApplication: Application?
var body: some View {
List(applications, selection: $selectedApplication) { application in
NavigationLink(value: application) {
Text(application.name)
}
}
// This works, but looks a bit complicated and... ugly
.onReceive(applications.publisher) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
if selectedApplication == nil {
selectedApplication = applications.first
}
}
}
// This also does not work, as the data is not yet available
.onAppear {
selectedApplication = applications.first
}
}
}
I'm currently preselecting the first Application item (if it exists) using the shown onReceive code, but it looks complicated and a bit ugly. For example, it only works properly when delaying the selection code.
Is there a better way to achieve this?
Thanks.
How about just setting the selectedApplication using the task modifier with an id as follows:
struct ApplicationsView: View {
#FetchRequest(fetchRequest: Application.all()) private var applications
#Binding var selectedApplication: Application?
var body: some View {
List(applications, selection: $selectedApplication) { application in
NavigationLink(value: application) {
Text(application.name!)
}
}
.task(id: applications.first) {
selectedApplication = applications.first
}
}
}
the task is fired when the view is first displayed, and when the id object is updated, so this works without introducing a delay
Related
I want to show the level of connectivity of the wifi depends on my input. I used custom wifi image in sfsymbol. The code result is not really good, so I am looking for a better way. Sorry about this if it's something that not possible in Xcode.
//please look my image atttachment
import SwiftUI
class ViewMode: ObservableObject {
#Published var rece = ""
}
struct MyApp: View {
#State private var passConnection: String = ""
var body: some View {
MyConnectionView(incomingConnection: $passConnection)
}
}
struct MyConnectionView: View {
#Binding var incomingConnection: String
var body: some View {
VStack {
VStack {
if incomingConnection == "low" {
CustomWifi1
}
else if incomingConnection == "medium" {
CustomWifi2
}
else {
CustomWifi3
}
}
}
}
var CustomWifi1: some View {
Image("custom1")
}
var CustomWifi2: some View {
Image("custom2")
}
var CustomWifi3: some View {
Image("custom3")
}
var CustomWifi4: some View {
Image("custom4")
}
var CustomWifi5: some View {
Image("custom5")
}
}
You can display a different level of connection with SwiftUI and the feature of SF Symbols by using "variableValue" modifier of the "Image".
Therefore, you don't have to create multiple customized wifi images for your app. Also, you should have included #swiftui tag in your question.
Try this code below:
import SwiftUI
struct TestView: View {
#State var connectionValue = 0.0
var body: some View {
VStack {
Slider(value: $connectionValue)
Image(systemName: "wifi", variableValue: connectionValue)
}
}
}
I'm currently having all sorts of problems with a NavigationView in my multi-platform SwiftUI app.
My goal is to have a NavigationView with an item for each object in a list from Core Data. And each NavigationLink should lead to a view that can read and write data of the object that it's showing.
However I'm running into many problems, so I figured I'm probably taking the wrong approach.
Here is my code as of now:
struct InstanceList: View {
#StateObject private var viewModel = InstancesViewModel()
#State var selectedInstance: Instance?
var body: some View {
NavigationView {
List {
ForEach(viewModel.instances) { instance in
NavigationLink(destination: InstanceView(instance: instance), tag: instance, selection: $selectedInstance) {
InstanceRow(instance)
}
}
.onDelete { set in
viewModel.deleteInstance(viewModel.instances[Array(set)[0]])
for reverseIndex in stride(from: viewModel.instances.count - 1, through: 0, by: -1) {
viewModel.instances[reverseIndex].id = Int16(reverseIndex)
}
}
}
.onAppear {
selectedInstance = viewModel.instances.first
}
.listStyle(SidebarListStyle())
.navigationTitle("Instances")
.toolbar {
ToolbarItemGroup {
Button {
withAnimation {
viewModel.addInstance(name: "4x4", puzzle: "3x3") // temporary
}
} label: {
Image(systemName: "plus")
}
}
}
}
}
}
and the view model (which probably isn't very relevant but I'm including it just in case):
class InstancesViewModel: ObservableObject {
#Published var instances = [Instance]()
private var cancellable: AnyCancellable?
init(instancePublisher: AnyPublisher<[Instance], Never> = InstanceStorage.shared.instances.eraseToAnyPublisher()) {
cancellable = instancePublisher.sink { instances in
self.instances = instances
}
}
func addInstance(name: String, puzzle: String, notes: String? = nil, id: Int? = nil) {
InstanceStorage.shared.add(
name: name,
puzzle: puzzle,
notes: notes,
id: id ?? (instances.map{ Int($0.id) }.max() ?? -1) + 1
)
}
func deleteInstance(_ instance: Instance) {
InstanceStorage.shared.delete(instance)
}
func deleteInstance(withId id: Int) {
InstanceStorage.shared.delete(withId: id)
}
func updateInstance(_ instance: Instance, name: String? = nil, puzzle: String? = nil, notes: String? = nil, id: Int? = nil) {
InstanceStorage.shared.update(instance, name: name, puzzle: puzzle, notes: notes, id: id)
}
}
and then the InstanceView, which just shows some simple information for testing:
struct InstanceView: View {
#ObservedObject var instance: Instance
var body: some View {
Text(instance.name)
Text(String(instance.id))
}
}
Some of the issues I'm having are:
On iOS and iPadOS, when the app starts, it will show a blank InstanceView, pressing the back button will return to a normal instanceView and pressing it again will show the navigationView
Sometime pressing on a navigationLink will only highlight it and won't go to the destination
On an iPhone in landscape, when scrolling through the NavigationView, sometimes the selected Item will get unselected.
When I delete an item, the InstanceView shows nothing for the name and 0 for the id, as if its showing a "ghost?" instance, until you select a different one.
I've tried binding the selecting using the index of the selected Instance but that still has many of the same problems.
So I feel like I'm making some mistake in the way that I'm using NavigationView, and I was wondering what the best approach would be for creating a navigationView from an Array that works nicely across all devices.
Thanks!
I know that State wrappers are for View and they designed for this goal, but I wanted to try build and test some code if it is possible, my goal is just for learning purpose,
I have 2 big issues with my code!
Xcode is unable to find T.
How can I initialize my state?
import SwiftUI
var state: State<T> where T: StringProtocol = State(get: { state }, set: { newValue in state = newValue })
struct ContentView: View {
var body: some View {
Text(state)
}
}
Update: I could do samething for Binding here, Now I want do it for State as well with up code
import SwiftUI
var state2: String = String() { didSet { print(state2) } }
var binding: Binding = Binding.init(get: { state2 }, set: { newValue in state2 = newValue })
struct ContentView: View {
var body: some View {
TextField("Enter your text", text: binding)
}
}
If I could find the answer of my issue then, i can define my State and Binding both outside of View, 50% of this work done and it need another 50% for State Wrapper.
New Update:
import SwiftUI
var state: State<String> = State.init(initialValue: "Hello") { didSet { print(state.wrappedValue) } }
var binding: Binding = Binding.init(get: { state.wrappedValue }, set: { newValue in state = State(wrappedValue: newValue) })
struct ContentView: View {
var body: some View {
Text(state) // <<: Here is the issue!
TextField("Enter your text", text: binding)
}
}
Even if you create a State wrapper outside a view, how will the view know when to refresh its body?
Without a way to notify the view, your code will do the same as:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}
What you can do next depends on what you want to achieve.
If all you need is a way to replicate the State behaviour outside the view, I recommend you take a closer look at the Combine framework.
An interesting example is CurrentValueSubject:
var state = CurrentValueSubject<String, Never>("state1")
It stores the current value and also acts as a Publisher.
What will happen if we use it in a view that doesn't observe anything?
struct ContentView: View {
var body: some View {
Text(state.value)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
}
}
The answer is: nothing. The view is drawn once and, even if the state changes, the view won't be re-drawn.
You need a way to notify the view about the changes. In theory you could do something like:
var state = CurrentValueSubject<String, Never>("state1")
struct ContentView: View {
#State var internalState = ""
var body: some View {
Text(internalState)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
.onReceive(state) {
internalState = $0
}
}
}
But this is neither elegant nor clean. In these cases we should probably use #State:
struct ContentView: View {
#State var state = "state1"
var body: some View {
Text(state)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state = "state2"
}
}
}
}
To sum up, if you need a view to be refreshed, just use the native SwiftUI property wrappers (like #State). And if you need to declare state values outside the view, use ObservableObject + #Published.
Otherwise there is a huge Combine framework which does exactly what you want. I recommend you take a look at these links:
Combine: Getting Started
Using Combine
I'm quite new to SwiftUI and I'm wondering how I should use a picker in an update view correctly.
At the moment I have a form and load the data in with .onAppear(). That works fine but when I try to pick something and go back to the update view the .onAppear() gets called again and I loose the picked value.
In the code it looks like this:
import SwiftUI
struct MaterialUpdateView: View {
// Bindings
#State var material: Material
// Form Values
#State var selectedUnit = ""
var body: some View {
VStack(){
List() {
Section(header: Text("MATERIAL")){
// Picker for the Unit
Picker(selection: $selectedUnit, label: Text("Einheit")) {
ForEach(API().units) { unit in
Text("\(unit.name)").tag(unit.name)
}
}
}
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Does anyone has experience with that problem or am I doing something terribly wrong?
You need to create a custom binding which we will implement in another subview. This subview will be initialised with the binding vars selectedUnit and material
First, make your MaterialUpdateView:
struct MaterialUpdateView: View {
// Bindings
#State var material : Material
// Form Values
#State var selectedUnit = ""
var body: some View {
NavigationView {
VStack(){
List() {
Section(header: Text("MATERIAL")) {
MaterialPickerView(selectedUnit: $selectedUnit, material: $material)
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Then, below, add your MaterialPickerView, as shown:
Disclaimer: You need to be able to access your API() from here, so move it or add it in this view. As I have seen that you are re-instanciating it everytime, maybe it is better that you store its instance with let api = API() and then refer to it with api, and even pass it to this view as such!
struct MaterialPickerView: View {
#Binding var selectedUnit: String
#Binding var material : Material
#State var idx: Int = 0
var body: some View {
let binding = Binding<Int>(
get: { self.idx },
set: {
self.idx = $0
self.selectedUnit = API().units[self.idx].name
self.material.unit = self.selectedUnit
})
return Picker(selection: binding, label: Text("Einheit")) {
ForEach(API().units.indices) { i in
Text(API().units[i].name).tag(API().units[i].name)
}
}
}
}
That should do,let me know if it works!
I am building a complex interface in SwiftUI that I need to break into multiple extensions in order to be able to compile the code, but I can't figure out how to pass data between the extension and the body structure.
I made a simple code to explain it :
class Search: ObservableObject {
#Published var angle: Int = 10
}
struct ContentView: View {
#ObservedObject static var search = Search()
var body: some View {
VStack {
Text("\(ContentView.self.search.angle)")
aTest()
}
}
}
extension ContentView {
struct aTest: View {
var body: some View {
ZStack {
Button(action: { ContentView.search.angle = 11}) { Text("Button")}
}
}
}
}
When I press the button the text does not update, which is my issue. I really appreciate any help you can provide.
You can try the following:
struct ContentView: View {
#ObservedObject var search = Search()
var body: some View {
VStack {
Text("\(ContentView.self.search.angle)")
aTest // call as a computed property
}
}
}
extension ContentView {
var aTest: some View { // not a separate `struct` anymore
ZStack {
Button(action: { self.search.angle = 11 }) { Text("Button")}
}
}
}