Extraneous Argument with Binding in SwiftUI - swiftui

I was playing with binding in playground using SwiftUI, it's a very simple code
struct Car:View {
var kar = "🚚"
#Binding var startAnimation: Bool = false
var body: some View {
HStack {
Spacer()
Text(kar)
.font(.custom("Arial", size: 100))
.offset(x: self.startAnimation ? 0 - UIScreen.main.bounds.width + 100: 0)
}
}
}
But playground gave an error that says "Extraneous Argument Label ", I literally see no problem, especially when i code using Xcode. so is there a way to somehow hack it or did i miss something??

You can not or you should not give direct value to Binding, it bind data, you should use State for example like in code
with State:
import SwiftUI
struct CarView: View {
#State private var startAnimation: Bool = Bool()
var body: some View {
VStack {
Text("🚚")
.font(Font.system(size: 100))
.offset(x: startAnimation ? 50.0 - UIScreen.main.bounds.width/2 : UIScreen.main.bounds.width/2 - 50.0)
Button("Drive!") { startAnimation.toggle() }
.font(Font.headline)
.padding()
}
.animation(.easeIn(duration: 3.0), value: startAnimation)
}
}
With Binding:
import SwiftUI
struct ContentView: View {
#State private var startAnimation: Bool = Bool()
var body: some View {
VStack {
CarView(startAnimation: $startAnimation)
Button("Drive!") {
startAnimation.toggle()
}
.font(Font.headline)
.padding()
}
.animation(.easeIn(duration: 3.0), value: startAnimation)
}
}
struct CarView: View {
#Binding var startAnimation: Bool
var body: some View {
Text("🚚")
.font(Font.system(size: 100))
.offset(x: startAnimation ? 50.0 - UIScreen.main.bounds.width/2 : UIScreen.main.bounds.width/2 - 50.0)
}
}

Related

LazyGridView how to detect and act on item overflowing screen?

I have a grid of items. Each item can expand height. I want to autoscroll when the item is expanded so it doesn't overflow the screen.
I was successful with the following code but I had to revert to a hack.
The idea was to detect when the item is overflowing using a Geometry reader on the item's background. Works wonders.
The issue is that when the view is expanded , the geo reader will update after the condition to check if autoscroll should execute is ran by the dispatcher. Hence my ugly hack.
Wonder what is the proper way ?
import SwiftUI
struct BlocksGridView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 300, maximum: .infinity), spacing: 20)]
var body: some View {
ZStack{
ScrollView {
ScrollViewReader { value in
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<20), id: \.self) {
BlockView(cardID: $0,scrollReader: value).id($0)
}
}
}
.padding(20)
}
}
}
}
struct BlockView : View {
var cardID : Int
var scrollReader : ScrollViewProxy
#State private var isOverflowingScreen = false
#State private var expand = false
var body: some View {
ZStack{
Rectangle()
.foregroundColor(isOverflowingScreen ? Color.blue : Color.green)
.frame(height: expand ? 300 : 135)
.clipShape(Rectangle()).cornerRadius(14)
.overlay(Text(cardID.description))
.background(GeometryReader { geo -> Color in
DispatchQueue.main.async {
if geo.frame(in: .global).maxY > UIScreen.main.bounds.maxY {
isOverflowingScreen = true
} else {
isOverflowingScreen = false
}
}
return Color.clear
})
.onTapGesture {
expand.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <-- Hack :(
if isOverflowingScreen {
withAnimation{
scrollReader.scrollTo(cardID)
}
}
}
}
}
}
}
struct BlocksGridView_Previews: PreviewProvider {
static var previews: some View {
BlocksGridView()
}
}
Blue items are overflowing ...

TabView causing preview to crash

I am currently trying to make a featured games section in my app. I am using the TabView in SwiftUI to do this, but am running into an issue where the preview crashes from it. I am not getting any errors and also am unable to run it live. Below is the code of the view causing the preview crash.
import SwiftUI
struct FeaturedGamesView: View {
var numberOfImages: Int
#ObservedObject var featuredGames: GameQuery
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack(alignment: .top, spacing: 0) {
TabView() {
ForEach(featuredGames.games.results) { game in
NavigationLink(destination: NavigationLazyView(GameDetailsView(gameDetailsQuery: GameQuery(gameName: game.slug)))){
GameItem(game: game)
}
}
}
.offset(x: (CGFloat(self.currentIndex) * -185), y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % 19
}
}
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
struct FeaturedGamesView_Previews: PreviewProvider {
static var previews: some View {
FeaturedGamesView(numberOfImages: 3, featuredGames: GameQuery(gameCategory: GameCategory.featured))
}
}
From some of my experimentation it seems to either not like the observable object I am using to get an array of data to populate the tabview. Any help would be greatly appreciated.
Update: I created a simpler example which still produces the error. In this case everything is generic except the array being used which is an observable object. Switching out the data structure used in the ForEach fixes it, but am looking to understand why my observable object does not work here.
import SwiftUI
struct Test: View {
#State private var selection = 0
#ObservedObject var featuredGames: GameQuery
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack {
TabView() {
ForEach(featuredGames.games.results) { game in
Text("Hi")
}
}.frame(height: 170)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test(featuredGames: GameQuery(gameCategory: GameCategory.featured))
}
}
Please see below code.
I fixed below part to simple then it works.
Maybe this has a problem.
ForEach(featuredGames.games.results) { game in
NavigationLink(destination: NavigationLazyView(GameDetailsView(gameDetailsQuery: GameQuery(gameName: game.slug)))){
GameItem(game: game)
}
}
Working code (I made my self simple GameQuery observableObject)
import SwiftUI
struct FeaturedGamesView: View {
var numberOfImages: Int
#ObservedObject var featuredGames: GameQuery
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack(alignment: .top, spacing: 0) {
TabView() {
ForEach(featuredGames.results, id:\.self) { game in
NavigationLink(destination: VStack {}){
Text("Hello")
}
}
}
.offset(x: (CGFloat(self.currentIndex) * -185), y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % 19
}
}
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
final class GameQuery: ObservableObject {
var gameCagetory: Int = 0
var results: [Int] = [1,2,3]
}
struct FeaturedGamesView_Previews: PreviewProvider {
static var previews: some View {
FeaturedGamesView(numberOfImages: 3, featuredGames: GameQuery())
}
}

SwiftUI View Not Updating to Environment Object

I am having an issue getting the ContentBodyView to update properly when a button is pressed in the MenuView.
When I print out the #Published currentPage var, it changes on button press, but doesn't appear to get carried across into ContentBodyView. Am I missing something?
I wrote the code based off of this tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
I've tried doing an .onReceive event on ContentBodyView, but the view still did not change.
App.swift:
#main
struct App: App {
#StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewRouter)
}
}
}
ContentView:
struct ContentView: View {
// MARK: - PROPERTIES
// for future use...
#State var width = UIScreen.main.bounds.width - 90
// to hide view...
#State var x = -UIScreen.main.bounds.width + 90
#EnvironmentObject var viewRouter: ViewRouter
// MARK: - BODY
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) {
ContentBodyView(x: $x)
.environmentObject(ViewRouter())
MenuView(x: $x)
.shadow(color: Color.black.opacity(x != 0 ? 0.1 : 0), radius: 5, x: 5, y: 0)
.offset(x: x)
.background(Color.black.opacity(x == 0 ? 0.5 : 0).ignoresSafeArea(.all, edges: .vertical).onTapGesture {
// hiding the view when back is pressed...
withAnimation {
x = -width
}
}) //: background
.environmentObject(ViewRouter())
} //: ZSTACK
// adding gesture or drag feature...
.gesture(DragGesture().onChanged({ (value) in
withAnimation {
if value.translation.width > 0 {
// disabling over drag...
if x < 0 {
x = -width + value.translation.width
}
} else {
if x != -width {
x = value.translation.width
}
}
}
}).onEnded({ (value) in
withAnimation {
// checking if half the value of menu is dragged means setting x to 0...
if -x < width / 2 {
x = 0
} else {
x = -width
}
}
})) //: GESTURE
} //: VSTACK
}
}
MenuView:
struct MenuView: View {
// MARK: - PROPERTIES
var edges = UIApplication.shared.windows.first?.safeAreaInsets
// for future use...
#State var width = UIScreen.main.bounds.width - 90
// to hide view...
#Binding var x: CGFloat
#EnvironmentObject var viewRouter: ViewRouter
// MARK: - BODY
var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading) {
HStack{
Button(action: {
withAnimation {
x = -width
}
}) {
Image(systemName: "xmark")
.resizable()
.frame(width: 18, height: 18)
.padding()
.padding(.top, 25)
.foregroundColor(Color.black)
}
Spacer()
}
ForEach(menuData) { item in
Button(action: {
withAnimation {
if (item.router == "shareables") {
viewRouter.currentPage = .shareables
} else {
viewRouter.currentPage = .home
}
x = -width
}
}) {
Text("\(item.label)")
} //: BUTTON
} //: FOREACH
} //: VSTACK
.padding(.horizontal,20)
// since vertical edges are ignored....
.padding(.top,edges!.top == 0 ? 15 : edges?.top)
.padding(.bottom,edges!.bottom == 0 ? 15 : edges?.bottom)
// default width...
.frame(width: UIScreen.main.bounds.width - 90)
.background(Color.white)
.ignoresSafeArea(.all, edges: .vertical)
Spacer(minLength: 0)
} //: HSTACK
}
}
ContentBodyView:
struct ContentBodyView: View {
// MARK: - PROPERTIES
#EnvironmentObject var viewRouter: ViewRouter
#Binding var x : CGFloat
// MARK: - BODY
var body: some View{
VStack {
switch viewRouter.currentPage {
case .home:
NavigationBarView(x: $x, title: "Home")
Spacer()
HomeView()
.transition(.scale)
case .shareables:
NavigationBarView(x: $x, title: "Shareables")
Spacer()
ShareablesView()
.transition(.scale)
} //: SWITCH
} //: VSTACK
// for drag gesture...
.contentShape(Rectangle())
.background(Color.white)
}
}
ViewRouter:
final class ViewRouter: ObservableObject {
#Published var currentPage: Page = .home
}
enum Page {
case home
case shareables
}
Try using the same ViewRouter instance in all views.
The following code creates new instances of ViewRouter:
ContentBodyView(x: $x)
.environmentObject(ViewRouter())
MenuView(x: $x)
...
.environmentObject(ViewRouter())
Replace them with:
#EnvironmentObject var viewRouter: ViewRouter
...
.environmentObject(viewRouter)
But in reality you usually need to inject .environmentObject only once per environment, so all these calls may be unnecessary.
Injecting the ViewRouter once in ContentView should be enough (if you're not using sheet etc):
#StateObject var viewRouter = ViewRouter()
...
ContentView().environmentObject(viewRouter)

Initializing 2 variables in init(): 'self' used before all stored properties are initialized

I'm trying to initialize 2 variables
self.customerVM = BuildCustomerViewModel()
self.showSurvey = showSurvey.wrappedValue
inside of my init() function and xCode returns
'self' used before all stored properties are initialized
When i try to initialize just the 1st one and do not use the 2nd variable - everything goes smoothly.. I don't understand why..
I wonder how should i change the code to make it work. Any help is appreciated.
import SwiftUI
struct BuildCustomerView: View {
#EnvironmentObject var thisSession: CurrentSession
#ObservedObject var customerVM: BuildCustomerViewModel
#State var fitnessLevel: Double = 0.0
#Binding var showSurvey: Bool
init(showSurvey: Binding<Bool>) {
self.customerVM = BuildCustomerViewModel()
self.showSurvey = showSurvey.wrappedValue
}
var body: some View {
ZStack {
VStack (alignment: .leading) {
VStack {
Text("What is your fitness level?")
.font(.headline)
}
HStack (alignment: .top) {
Slider(value: self.$fitnessLevel, in: -1...1, step: 0.1)
}
.frame(height: 50)
// save changes
Rectangle()
.fill( Color.blue )
.frame(height: 150, alignment: .leading)
.overlay(
Text("Next")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.white)
)
.onTapGesture {
self.customerVM.insertCustomerData(userId: self.thisSession.userId!, customerData: CustomerData(fitnessLevel: self.fitnessLevel)) { success in
if success == true {
print("FitnessLevel update succeed")
self.showSurvey.wrappedValue = false
} else {
print("FitnessLevel update failed")
}
}
}
Spacer()
}
.padding(.top, 30)
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
}
}
}
MainViewWrapper code, where this view is called from:
struct MainViewWrapper: View {
#EnvironmentObject var thisSession: CurrentSession
#ObservedObject var mainData: MainViewModel
// show profile if all data is loaded
#State var showProfile: Bool = false
#State var showSurvey: Bool = false
#State var selection: String? = nil
init(mainData: MainViewModel) {
self.mainData = mainData
}
var body: some View {
NavigationView {
ZStack {
ProfileView()
.opacity(self.showProfile ? 1 : 0)
BuildCustomerView(showSurvey: self.$showSurvey)
.opacity(self.showSurvey ? 1 : 0)
}
}
}
}
Binding as a property (hidden) has _ (underscore), so you have to initialize it as
init(showSurvey: Binding<Bool>) {
self.customerVM = BuildCustomerViewModel()
self._showSurvey = showSurvey // << this !!
}

SwiftUI - Animation in view stops when scrolling the list

Considering the following code, why the animations in the views that are initialized without the n property stops when you scroll the list?
Tested on Xcode 11.3 (11C29) with a new default project on device and simulator.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
List(1...50, id: \.self) { n in
HStack {
KeepRolling()
Spacer()
KeepRolling(n: n)
}
}
}
}
}
struct KeepRolling: View {
#State var isAnimating = false
var n: Int? = nil
var body: some View {
Rectangle()
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0))
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.isAnimating = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
IMO it is due to caching/reuse in List. For List all the values of KeepRolling() is the same, so .onAppear is not always re-called.
If to make every such view unique, say using .id as below, all works (tested with Xcode 11.2)
KeepRolling().id(UUID().uuidString)