Just started learning SwiftUI and reading a book, written around mid 2020 using iOS13, which says a conditional view needs to wrapped inside a view builder otherwise it won't compile because SwiftUI won't know what type of the view is. For example, it says the following won't compile but I just tried in Xcode 13, iOS15 and it compiles fine. So has view builder been updated since iOS13?
struct TestView: View {
#Binding var n: Int
var body: some View {
if n > 0 {
Text("Tapped \(n) times.")
}
}
}
Related
I've been following a youtube tutorial for using LazyVStacks.
https://www.youtube.com/watch?v=o6D7mUXjSmI
When I run the same code as per the tutorial, the LazyVStack using Xcode Version 13.2.1 (13C100) on a iPhone 11 Pro Max (running iOS 15.2) prints out 83 statements, when only 42 rows are in view. As per the tutorial it should only print 42 statements to the console.
Is this an Xcode/iOS bug? I'm unable to download the latest Xcode as my Mac doesn't support macOS 12.0 to verify.
Video tutorial showing on 42 print statements in the console on iPhone 11 Pro Max:
My code showing 83 print statements in the console on iPhone 11 Pro Max
import SwiftUI
struct SampleRow: View {
let id: Int
var body: some View {
Text("Row \(id)")
}
init(id: Int) {
print("Loading row \(id)")
self.id = id
}
}
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(1...1000, id: \.self, content: SampleRow.init)
}
}
}
}
I got the exact same problem, I've got several views that render and fetch data dynamically upon reaching the bottom of the page but for some unknown reason, the same exact code is not working on a view where all the data are rendered at once.. I don't know if it's a bug in LazyVStack implementation or anything else but the behavior is ambiguous
It's working fine in Xcode 13. I'm not sure why you need a LazyVStack, unless you're using some sort of grid. I slightly modified the code based on there's no much use for it here. It's printing all 1000.
import SwiftUI
struct SampleRow: View {
let id: Int
var body: some View {
Text("Row \(id)")
}
init(id: Int) {
print("Loading row \(id)")
self.id = id
}
}
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1...1000, id: \.self) { index in
SampleRow(id: index)
}
}
}
}
I have a enum that defines the language index. I put this enum in a file named Language.swift
enum Language: Int {
case en
case zh_hant
}
I have tried to declare a global variable that stores the current language.
final class ModelData: ObservableObject {
#Published var currentLanguage: Language = Language.zh_hant
#Published var globalString: [String] = load("strings.json")
}
However, when I tried to access that, I have the following error:
struct HomeView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
Text("\(modelData.currentLanguage.rawValue)") // error, see the screen capture
Text("\(modelData.globalString.count)") // no problem
}
}
Yet, I used the same way to access the array, there is no problem.
The above error can be resolved by moving enum Language to be in the same file as class ModelData.
Yet, another problem was then identified.
I tried to do this in my code:
var languageIndex: Int {
modelData.currentLanguage.rawValue
}
var body: some View {
Text("\(modelData.globalString[languageIndex])") // preview cause "updating took more than 5 seconds]
}
My global String like this
["Hello", "你好"]
The problem appears in the Canvas view on preview the UI.
Yet, it seems to work fine under simulator. Any idea?
The problem appears in the Canvas view on preview the UI. Yet, it seems to work fine under simulator.
You have to set the environment object in the preview
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
.environmentObject(ModelData())
}
}
I have an array of items (numbers) to be presented to the user using NavigationView, List and a leaf page.
When I update an item (numbers[index] = ...) on a leaf page, it updates the list correctly (which I see when I go back to the list), but not the leaf page itself immediately. I see the change if I go back to the list and re-open the same leaf page.
I would like to understand why it does not update the UI immediately, and how to fix it. Here is the simplified code to re-produce this behavior.
ADDITIONAL INFORMATION
This code works fine on Xcode 12. It fails only on Xcode 12.1 (RC1) and Xcode 12.2 (beta3).
import SwiftUI
struct NumberHolder: Identifiable {
let id = UUID()
let value:Int
}
struct Playground: View {
#State var numbers:[NumberHolder] = [
NumberHolder(value:1),
NumberHolder(value:2),
NumberHolder(value:3),
NumberHolder(value:4),
]
var body: some View {
NavigationView {
List(numbers.indices) { index in
let number = numbers[index]
NavigationLink(destination: VStack {
Text("Number: \(number.value)")
Button("Increment") {
numbers[index] = NumberHolder(value: number.value + 1)
}
} ) {
Text("Number: \(number.value)")
}
}
}
}
}
struct Playground_Previews: PreviewProvider {
static var previews: some View {
Playground()
}
}
Update
Apple has since replied to my Issue stating they have resolved this since watchOS 8 beta 3. I've tested this on WatchOS 9 and iOS 16 and this is indeed now working correctly.
Previous answer:
This had me scratching my day for a few weeks.
It appears there are many features within SwiftUI that do not work in Views that are placed in Lists directly, however if you add a ForEach inside the List said features (such as .listRowPlatterColor(.green) on WatchOS) start to work.
Solution
On iOS 14.2 if you wrap the NavigationLink inside a ForEach the NavigationLink destination (leaf page) will update right away when the data model is updated.
So change your code to
var body: some View {
NavigationView {
List {
ForEach(numbers.indices) { index in
let number = numbers[index]
NavigationLink(destination: VStack {
Text("Number: \(number.value)")
Button("Increment") {
numbers[index] = NumberHolder(value: number.value + 1)
}
} ) {
Text("Number: \(number.value)")
}
}
}
}
}
Frustratingly, this does not solve the issue when using WatchOS, in WatchOS 7.0 the leaf page is updated, however in WatchOS 7.1 (version goes hand in hand with iOS 14.2 that suffered this "issue") so I have an issue open with Apple FB8892330
Further frustratingly, I still don't know if this is a bug or a feature in SwiftUI, none of the documentation state the requirement for ForEach inside of Lists
Try this one. I tested and it works.
import SwiftUI
struct NumberHolder: Identifiable {
let id = UUID()
let value:Int
}
struct ContentView: View {
#State var numbers:[NumberHolder] = [
NumberHolder(value:1),
NumberHolder(value:2),
NumberHolder(value:3),
NumberHolder(value:4),
]
var body: some View {
NavigationView {
List(numbers.indices) { index in
NavigationLink(destination: DetailView(numbers: $numbers, index: index)) {
Text("Number: \(self.numbers[index].value)")
}
}
}
}
}
struct DetailView: View {
#Binding var numbers:[NumberHolder]
let index: Int
var body: some View {
VStack {
Text("Number: \(self.numbers[index].value)")
Button("Increment") {
numbers[index] = NumberHolder(value: numbers[index].value + 1)
}
}
}
}
I found the answer. It was a bug in Xcode.
This code (without any changes) works fine under Xcode 12.0, but fails to update under Xcode 12.2 beta 2.
I'm trying to use the new property wrapper for iOS 14 "SceneStorage" though it is producing this error:
Fatal error: #SceneStorage is only for use with SwiftUI App
Lifecycle.: file SwiftUI, line 0
Here is my code:
struct ContentView: View {
#SceneStorage("isLoggedIn") var isLoggedIn = true
var body: some View {
Text("Hello, World!).onAppear {
print($isLoggedIn)
}
}
}
Updated....
Thanks, #Asperi!.
Make sure you set the Life Cycle to SwiftUI App.
Works fine with Xcode 12 / iOS 14 / SwiftUI Life-cycle
It looks like your project uses SwiftUI 1.0 AppDelegate/SceneDelegate (aka UIKit Life-cycle)
In SwiftUI Life-cycle your app main should be like
#main
struct YourAppName: App {
var body: some Scene {
WindowGroup { // << this introduces Scene, needed for SceneStorage
ContentView()
}
}
}
I'm using Xcode 11 GM
The hierarchy I have is:
List > Form (Picker)
This is the code for List:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
List {
NavigationLink(destination: FormView())
{
Text("Item 1")
}
}
.navigationBarTitle("List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And this is the code for the Form:
import SwiftUI
struct FormView: View {
var body: some View {
Form {
Picker(selection: .constant(1), label: Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
}
.navigationBarTitle("Form")
}
}
struct FormView_Previews: PreviewProvider {
static var previews: some View {
FormView()
}
}
The problem is:
When I build on iPad split view, tap to select works as expected:
But, when inside tags I cannot select them, nor it will go back to form view:
On iPhone, it works fine...
Is this a known bug?
Cheers to all
In my experience, it seems that the picker cells expect the user to tap on the Text elements themselves.
I don't know why. I think it's weird, too, and probably a bug. Not sure Apple knows about it yet. I sure haven't filed a radar.
To confirm this, try testing with longer text in the picker items.
What I normally do is implement a simple picker myself (not that hard, just a List and some items) and implement the items as Buttons. (If it changes the color on me, I change it back using .foregroundColor(.primary)). What's nice about this is that a Button inside a Form or a List renders as the table view cell we're used to, with the same highlighting interaction we all know and love!