Related
I have a video player that starts playing a video (from firebase URL) and in some cases (70% of cases) i get this error message (exception) when running on physical device (no issues when launching in simulator though):
"CALayer position contains NaN: [nan nan]"
I found that the error doesn't appear when i comment "VideoPlayerControlsView()", so i'm pretty sure the problem is is my CustomerSlider object located insider of this VideoPlayerControlsView view.
I think it may somehow be caused by loading remote video, as the video is not loaded, the app doesn't know the size/bounds of AVPlayer object and therefore some parent view (maybe CustomerSlider) can't be created..
Building a Minimal Reproducible Example would be a nightmare, i just hope some can find a mistake in my code/logic.. If not - gonna build it of course. No other choice.
struct DetailedPlayerView : View {
// The progress through the video, as a percentage (from 0 to 1)
#State private var videoPos: Double = 0
// The duration of the video in seconds
#State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
#State private var seeking = false
private var player: AVPlayer = AVPlayer()
init(item: ExerciseItem, hVideoURL: URL?) {
if hVideoURL != nil {
player = AVPlayer(url: hVideoURL!)
player.isMuted = true
player.play()
} else {
print("[debug] hVideoURL is nil")
}
}
var body: some View {
ZStack {
//VStack {
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
//timeline: $timeline,
//videoTimeline: videoTimeline,
player: player)
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth)
VStack {
Spacer()
VideoPlayerControlsView(videoPos: $videoPos, **<<-----------------------**
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.frame(width: UIScreen.screenHeight - 2*Constants.scrollPadding, height: 20)
.padding(.bottom, 20)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
}
struct VideoPlayerControlsView : View {
#Binding private(set) var videoPos: Double
#Binding private(set) var videoDuration: Double
#Binding private(set) var seeking: Bool
// #Binding private(set) var timeline: [Advice]
#State var shouldStopPlayer: Bool = false
#State var player: AVPlayer
//let player: AVPlayer
#State private var playerPaused = false
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
.foregroundColor(Color.mainSubtitleColor)
.contentShape(Rectangle())
.padding(.trailing, 10)
}
// Current video time
if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
// Slider for seeking / showing video progress
CustomSlider(value: $videoPos, shouldStopPlayer: self.$shouldStopPlayer, range: (0, 1), knobWidth: 4) { modifiers in
ZStack {
Group {
Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5799999833106995))//Color((red: 0.4, green: 0.3, blue: 1)
.opacity(0.4)
.frame(height: 4)
.modifier(modifiers.barRight)
Color.mainSubtitleColor//Color(red: 0.4, green: 0.3, blue: 1)
.frame(height: 4)
.modifier(modifiers.barLeft)
}
.cornerRadius(5)
VStack {
Image(systemName: "arrowtriangle.down.fill") // SF Symbol
.foregroundColor(Color.mainSubtitleColor)
.offset(y: -3)
}
.frame(width: 20, height: 20)
.contentShape(Rectangle())
.modifier(modifiers.knob)
}
}
.onChange(of: shouldStopPlayer) { _ in
if shouldStopPlayer == false {
print("[debug] shouldStopPlayer == false")
sliderEditingChanged(editingStarted: false)
} else {
if seeking == false {
print("[debug] shouldStopPlayer == true")
sliderEditingChanged(editingStarted: true)
}
}
}
.frame(height: 20)
// Video duration
if videoDuration.isCanonical && videoDuration.isFinite {
Text(Utility.formatSecondsToHMS(videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
}
.padding(.leading, 40)
.padding(.trailing, 40)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}
extension Double {
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
// Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
}
struct CustomSliderComponents {
let barLeft: CustomSliderModifier
let barRight: CustomSliderModifier
let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
enum Name {
case barLeft
case barRight
case knob
}
let name: Name
let size: CGSize
let offset: CGFloat
func body(content: Content) -> some View {
content
.frame(width: (size.width >= 0) ? size.width : 0)
.position(x: size.width*0.5, y: size.height*0.5)
.offset(x: offset)
}
}
struct CustomSlider<Component: View>: View {
#Binding var value: Double
var range: (Double, Double)
var knobWidth: CGFloat?
let viewBuilder: (CustomSliderComponents) -> Component
#Binding var shouldStopPlayer: Bool
init(value: Binding<Double>, shouldStopPlayer: Binding<Bool>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: #escaping (CustomSliderComponents) -> Component
) {
_value = value
_shouldStopPlayer = shouldStopPlayer
self.range = range
self.viewBuilder = viewBuilder
self.knobWidth = knobWidth
}
var body: some View {
return GeometryReader { geometry in
self.view(geometry: geometry) // function below
}
}
private func view(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .global)
let drag = DragGesture(minimumDistance: 0)
.onChanged { drag in
shouldStopPlayer = true
self.onDragChange(drag, frame)
}
.onEnded { drag in
shouldStopPlayer = false
//self.updatedValue = value
print("[debug] slider drag gesture ended, value = \(value)")
}
let offsetX = self.getOffsetX(frame: frame)
let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5), height: frame.height)
let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
let modifiers = CustomSliderComponents(
barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
return ZStack { viewBuilder(modifiers).gesture(drag) }
}
private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
let xrange = (min: Double(0), max: Double(width.view - width.knob))
var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
value -= 0.5*width.knob // offset from center to leading edge of knob
value = value > xrange.max ? xrange.max : value // limit to leading edge
value = value < xrange.min ? xrange.min : value // limit to trailing edge
value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
//print("[debug] slider drag gesture detected, value = \(value)")
self.value = value
}
private func getOffsetX(frame: CGRect) -> CGFloat {
let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
let xrange: (Double, Double) = (0, Double(width.view - width.knob))
let result = self.value.convert(fromRange: range, toRange: xrange)
return CGFloat(result)
}
}
some extra code showing how DetailedPlayerView is triggered:
struct DetailedVideo: View {
var item: ExerciseItem
var url: URL
#Binding var isPaused: Bool
var body: some View {
ZStack {
DetailedPlayerView(item: self.item, hVideoURL: url)
//.frame(width: 500, height: 500) //##UPDATED: Apr 10
HStack {
VStack {
ZStack {
//Rectangle 126
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-135))
//Rectangle 125
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-45))
}
.frame(width: 35, height: 35)//14.4
.contentShape(Rectangle())
.onTapGesture {
print("[debugUI] isPaused = false")
self.isPaused = false
}
.offset(x:20, y:20)
Spacer()
}
Spacer()
}
}
.ignoresSafeArea(.all)
}
}
#ViewBuilder
var detailedVideoView: some View {
if self.hVideoURL != nil {
DetailedVideo(item: self.exerciseVM.exerciseItems[self.exerciseVM.currentIndex], url: self.hVideoURL!, isPaused: self.$exerciseVM.isPaused) // when is paused - we are playing detailed video?
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth) //UPDATED: Apr 9, 2021
.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
} else {
EmptyView()
}
}
I did my best to pull out only a small part of my larger project that displays this odd behavior. The intention is for one random number to be added to the array and displayed every 3 seconds. In iOS 13 each number slides in from the left every 3 seconds and everything works as expected. What I see in iOS 14 is that 4 numbers are added every 3 seconds. Does anyone understand why this would be happening? Thanks in advance!
import SwiftUI
struct ContentView: View {
#State private var calledNumbers = CalledNumbers()
#State private var timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var inProgress = false
var body: some View {
Button(action: {
if !self.inProgress {
self.calledNumbers.startOver()
print("Start timer")
self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
}
else {
print("Stop timer")
self.timer.upstream.connect().cancel()
}
self.inProgress.toggle()
})
{
if(self.inProgress == false) {
Text("S T A R T")
.font(.system(size: 22))
.fontWeight(.heavy)
.frame(width: 200, height: 35, alignment: .center)
.background(Capsule()
.fill(Color.green))
.cornerRadius(35)
.foregroundColor(.white)
.padding(.bottom, 2)
}
else {
Text("S T O P")
.font(.system(size: 22))
.fontWeight(.heavy)
.frame(width: 200, height: 35, alignment: .center)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(35)
.onReceive(self.timer) { _ in
self.timer.upstream.connect().cancel()
print("CALL NEXT NUMBER")
self.calledNumbers.callNextNumber()
self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
}
}
}
ZStack {
RoundedRectangle(cornerRadius: 35)
.frame(width: UIScreen.main.bounds.size.width - 19, height: 40, alignment: .center)
.foregroundColor(.clear)
.padding(.bottom, 2)
HStack {
ForEach(self.calledNumbers.calledNumberList.reversed().filter {self.checkCount(number: $0)}, id: \.self) { number in
Text("\(String(number))")
.font(.custom("Menlo", size: 20))
.fontWeight(.black)
.frame(width: 40, height: 40, alignment: .center)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(.white)
.transition(AnyTransition.offset(x: (number == self.calledNumbers.calledNumberList.last) ? -250 : 250))
.animation(Animation.linear(duration: 1).repeatCount(1))
}
}
}
}
func checkCount(number: Int) -> Bool {
let count = self.calledNumbers.calledNumberList.count
if (count <= 8) {
return true
}
else {
guard let index = self.calledNumbers.calledNumberList.firstIndex(of: number) else { return false }
if (count - index > 8) { return false }
else { return true }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This file was added by xcode 12 (I named this project Test2):
import SwiftUI
#main
struct Test2App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Class for calledNumbers
//
// class.swift
// Test2
//
import Foundation
class CalledNumbers {
#Published var calledNumberList: [Int]
init() {
calledNumberList = [Int]()
}
func callNextNumber() {
var tempNumber = Int.random(in: 1...75)
while calledNumberList.contains(tempNumber) {
tempNumber = Int.random(in: 1...75)
}
calledNumberList.append(tempNumber)
print("Number added \(tempNumber)")
}
func startOver() {
calledNumberList.removeAll()
}
}
The problem seems to be caused by having the .onReceived() attached to the code inside of the Button. Moving .onReceived() to the Button as a whole solves the issue.
Also, you were doing more timer manipulation than is necessary. I removed stopping and restarting the timer from .onReceive().
calledNumbers should be an #ObservableObject.
struct ContentView: View {
#ObservedObject var calledNumbers = CalledNumbers()
#State private var timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var inProgress = false
var body: some View {
Button(action: {
if !self.inProgress {
self.calledNumbers.startOver()
print("Start timer")
self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
}
else {
print("Stop timer")
self.timer.upstream.connect().cancel()
}
self.inProgress.toggle()
})
{
Text(inProgress ? "STOP" : "START")
.font(.system(size: 22))
.fontWeight(.heavy)
.frame(width: 200, height: 35, alignment: .center)
.cornerRadius(35)
.foregroundColor(.white)
.background(Capsule().fill(inProgress ? Color.red : .green))
.padding(.bottom, 2)
}
}
.onReceive(self.timer) { _ in
print("CALL NEXT NUMBER")
self.calledNumbers.callNextNumber()
}
.onAppear {
// Cancel the initial timer
self.timer.upstream.connect().cancel()
}
ZStack {
RoundedRectangle(cornerRadius: 35)
.frame(width: UIScreen.main.bounds.size.width - 19, height: 40, alignment: .center)
.foregroundColor(.clear)
.padding(.bottom, 2)
HStack {
ForEach(self.calledNumbers.calledNumberList.reversed().filter {self.checkCount(number: $0)}, id: \.self) { number in
Text("\(String(number))")
.font(.custom("Menlo", size: 20))
.fontWeight(.black)
.frame(width: 40, height: 40, alignment: .center)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(.white)
.transition(AnyTransition.offset(x: (number == self.calledNumbers.calledNumberList.last) ? -250 : 250))
.animation(Animation.linear(duration: 1).repeatCount(1))
}
}
}
}
func checkCount(number: Int) -> Bool {
let count = self.calledNumbers.calledNumberList.count
if (count <= 8) {
return true
}
else {
guard let index = self.calledNumbers.calledNumberList.firstIndex(of: number) else { return false }
if (count - index > 8) { return false }
else { return true }
}
}
}
Also, your CalledNumbers class should be an ObservableObject so that Published works correctly:
import Foundation
class CalledNumbers: ObservableObject {
#Published var calledNumberList: [Int]
init() {
calledNumberList = [Int]()
}
func callNextNumber() {
var tempNumber = Int.random(in: 1...75)
while calledNumberList.contains(tempNumber) {
tempNumber = Int.random(in: 1...75)
}
calledNumberList.append(tempNumber)
print("Number added \(tempNumber)")
}
func startOver() {
calledNumberList.removeAll()
}
}
I'm currently learning SwiftUI and building a todo list app. On the ContentView screen I've got a NavigationView and a button that pops up an "add new task" textfield into the list. I suspect this is not the correct way to implement this but when the textfield shows up the background color doesn't persist. For the life of me I can't figure out how to set the background color. If I move the textfield outside the NavigationView I can set the background but when the NavigationView shifts to make space for the textfield I get a bunch of black screen flicker. Any thoughts on how I can set the background color on the textfield when added to the list or fix the screen flicker when I move it out? Appreciate the help.
import SwiftUI
import UIKit
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]) var listItems: FetchedResults<ToDoItem>
#State private var newToDoItem = ""
#State private var showNewTask = false
#State var isEditing = false
#State var showTaskView = false
#State var bottomState = CGSize.zero
#State var showFull = false
#State var deleteButton = false
//this removes the lines in the list view
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
UIScrollView.appearance().backgroundColor = .clear
//UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack{
VStack{
TitleView()
NavigationView {
List {
if showNewTask {
HStack{
TextField("New task", text: self.$newToDoItem, onEditingChanged: { (changed) in
}) {
print("onCommit")
self.addTask(taskTitle: self.newToDoItem)
self.saveTasks()
self.showNewTask.toggle()
self.newToDoItem = ""
}
.font(Font.system(size: 18, weight: .bold))
.foregroundColor(Color("Text"))
Button(action: {
self.newToDoItem = ""
self.showNewTask.toggle()
}) {
Image(systemName: "xmark.circle").foregroundColor(Color("button"))
.font(Font.system(size: 18, weight: .bold))
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.background(Color("addNewTask"))
.cornerRadius(10.0)
}
ForEach(listItems, id: \.self) {item in
HStack {
Button(action: {
item.isComplete = true
self.saveTasks()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
self.deleteTaskTest(item: item)
}
}) {
if (item.isComplete) {
Image(systemName: "checkmark.circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color(#colorLiteral(red: 0.1616941956, green: 0.9244045403, blue: 0.1405039469, alpha: 1)))
.padding(.trailing, 4)
} else {
Image(systemName: "circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color("button"))
.padding(.trailing, 4)
}
}
.buttonStyle(PlainButtonStyle())
ToDoItemView(title: item.title, createdAt: "\(item.createdAt)")
.onTapGesture {
self.showTaskView.toggle()
}
.onLongPressGesture(minimumDuration: 0.1) {
self.isEditing.toggle()
print("this is a long press test")
}
}
.listRowBackground(Color("background"))
}
.onMove(perform: moveItem)
.onDelete(perform: deleteTask)
}
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
.navigationBarTitle(Text("ToDay"), displayMode: .large)
.navigationBarHidden(true)
.background(Color("background"))
}
//ADD A NEW TASK BUTTON
HStack {
Spacer()
Button(action: {
self.showNewTask.toggle()
}) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .bold))
.frame(width: 36, height: 36)
.background(Color("button"))
.foregroundColor(.white)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
.padding()
}
.blur(radius: showTaskView ? 20 : 0)
.animation(.default)
.padding(.top, 30)
//BOTTOM CARD VIEW
TaskView()
.offset(x: 0, y: showTaskView ? 360 : 1000)
.offset(y: bottomState.height)
.animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.5))
.gesture(
DragGesture().onChanged { value in
self.bottomState = value.translation
if self.showFull {
self.bottomState.height += -300
}
if self.bottomState.height < -300 {
self.bottomState.height = -300
}
} .onEnded { value in
if self.bottomState.height > 50 {
self.showTaskView = false
}
if (self.bottomState.height < -100 && !self.showFull) || (self.bottomState.height < -250 && self.showFull){
self.bottomState.height = -300
self.showFull = true
} else {
self.bottomState = .zero
self.showFull = false
}
}
)
}
.background(Color("background").edgesIgnoringSafeArea(.all))
}
Finally got it to work. For whatever reason reworking the stacks fixed it.
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]) var listItems: FetchedResults<ToDoItem>
#State private var showCancelButton: Bool = false
#State private var newToDoItem = ""
#State private var showNewTask = false
#State var isEditing = false
#State var showTaskView = false
#State var bottomState = CGSize.zero
#State var showFull = false
#State var deleteButton = false
var itemName = ""
init() {
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack {
VStack {
NavigationView {
VStack {
TitleView()
.padding(.top, 20)
.background(Color("background"))
// Enter new task view
if showNewTask {
HStack {
HStack {
TextField("New task", text: self.$newToDoItem, onEditingChanged: { (changed) in
}) {
self.addTask(taskTitle: self.newToDoItem)
self.saveTasks()
self.showNewTask.toggle()
self.newToDoItem = ""
}
.font(Font.system(size: 18, weight: .bold))
.foregroundColor(Color("Text"))
Button(action: {
self.newToDoItem = ""
self.showNewTask.toggle()
}) {
Image(systemName: "xmark.circle").foregroundColor(Color("button"))
.font(Font.system(size: 18, weight: .bold))
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.background(Color("addNewTask"))
.cornerRadius(10.0)
}
.background(Color("background"))
.padding(.horizontal)
}
List {
ForEach(listItems, id: \.self) {item in
HStack {
Button(action: {
item.isComplete = true
self.saveTasks()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
self.deleteTaskTest(item: item)
}
}) {
if (item.isComplete) {
Image(systemName: "checkmark.circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color(#colorLiteral(red: 0.1616941956, green: 0.9244045403, blue: 0.1405039469, alpha: 1)))
.padding(.trailing, 4)
} else {
Image(systemName: "circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color("button"))
.padding(.trailing, 4)
}
}
.buttonStyle(PlainButtonStyle())
ToDoItemView(title: item.title, createdAt: "\(item.createdAt)")
.onTapGesture {
//item.title = self.itemName
self.showTaskView.toggle()
}
.onLongPressGesture(minimumDuration: 0.1) {
self.isEditing.toggle()
print("this is a long press test")
}
}
.listRowBackground(Color("background"))
}
.onMove(perform: moveItem)
.onDelete(perform: deleteTask)
}
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
.navigationBarTitle(Text("ToDay"), displayMode: .large)
.navigationBarHidden(true)
.background(Color("background"))
}
.background(Color("background").edgesIgnoringSafeArea(.all))
}
HStack {
Spacer()
Button(action: {
//withAnimation(){
self.showNewTask.toggle()
//}
}) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .bold))
.frame(width: 36, height: 36)
.background(Color("button"))
.foregroundColor(.white)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
.padding()
}
.blur(radius: showTaskView ? 20 : 0)
//BOTTOM CARD VIEW
TaskView()
.offset(x: 0, y: showTaskView ? 360 : 1000)
.offset(y: bottomState.height)
.animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.5))
.gesture(
DragGesture().onChanged { value in
self.bottomState = value.translation
if self.showFull {
self.bottomState.height += -300
}
if self.bottomState.height < -300 {
self.bottomState.height = -300
}
} .onEnded { value in
if self.bottomState.height > 50 {
self.showTaskView = false
}
if (self.bottomState.height < -100 && !self.showFull) || (self.bottomState.height < -250 && self.showFull){
self.bottomState.height = -300
self.showFull = true
} else {
self.bottomState = .zero
self.showFull = false
}
}
)
}
.animation(.default)
.background(Color("background").edgesIgnoringSafeArea(.all))
}
func moveItem(indexSet: IndexSet, destination: Int){
let source = indexSet.first!
if source < destination {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = listItems[source].order
while startIndex <= endIndex {
listItems[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
listItems[source].order = startOrder
} else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = listItems[destination].order + 1
let newOrder = listItems[destination].order
while startIndex <= endIndex {
listItems[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
listItems[source].order = newOrder
}
saveTasks()
self.isEditing.toggle()
}
func deleteTask(indexSet: IndexSet){
let source = indexSet.first!
let listItem = listItems[source]
//self.deleteButton.toggle()
managedObjectContext.delete(listItem)
saveTasks()
}
func deleteTaskTest(item: ToDoItem){
managedObjectContext.delete(item)
saveTasks()
}
func addTask(taskTitle: String) {
let newTask = ToDoItem(context: managedObjectContext)
newTask.title = taskTitle
newTask.order = (listItems.last?.order ?? 0) + 1
newTask.createdAt = Date()
}
func saveTasks() {
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
I'm experiencing a strange behavior with wheel pickers embedded in a conditional subview. When the subview is shown, sometimes the values are not shown. Switching around between two conditional views, the values sometimes reappear. I've attached an animation showing the behavior and the full code. I can't find the reason for this.
Update: I've tried a lot of things to find the reason for this. Even after simplifying the revealing subform to just one single picker, replacing the foreach loop with hardcoded Text() entries, removing the framing and clipping on the picker, it still doesn't work.
import SwiftUI
fileprivate enum OpenSetting {
case none, start, end
}
struct ContentView: View {
#State private var openSetting = OpenSetting.none
#State private var startMinutes = 0
#State private var startSeconds = 10
#State private var endMinutes = 3
#State private var endSeconds = 0
var body: some View {
NavigationView {
Form {
// Start
TimeSetting(
title: "Start",
color: Color.yellow,
minutes: startMinutes,
seconds: startSeconds,
setting: .start,
openSetting: $openSetting
)
if openSetting == .start {
TimePicker(minutes: $startMinutes, seconds: $startSeconds)
}
// End
TimeSetting(
title: "End",
color: Color.green,
minutes: endMinutes,
seconds: endSeconds,
setting: .end,
openSetting: $openSetting
)
if openSetting == .end {
TimePicker(minutes: $endMinutes, seconds: $endSeconds)
}
}
.navigationBarTitle("Test")
.navigationBarItems(
trailing: Text("Start")
)
}
}
}
struct TimeSetting: View {
var title: String
var color: Color
var minutes: Int
var seconds: Int
fileprivate var setting: OpenSetting
fileprivate var openSetting: Binding<OpenSetting>
var body: some View {
HStack {
Text(title)
Spacer()
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(color)
Text(toTime(minutes: minutes, seconds: seconds))
}
.frame(width: 64, height: 32)
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation() {
self.openSetting.wrappedValue = (self.openSetting.wrappedValue == self.setting) ? OpenSetting.none : self.setting
}
}
}
func toTime(minutes: Int, seconds: Int) -> String {
let timeString = String(format: "%02d", minutes) + ":" + String(format: "%02d", seconds)
return timeString
}
}
struct TimePicker: View {
var minutes: Binding<Int>
var seconds: Binding<Int>
var body: some View {
HStack() {
Spacer()
Picker(selection: minutes, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Min.")
Picker(selection: seconds, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Sec.")
Spacer()
}
}
}
Using .id for Pickers seems helped. Tested with Xcode 11.2 / iOS 13.2.
var body: some View {
HStack() {
Spacer()
Picker(selection: minutes, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
.id(UUID().uuidString)
Text("Min.")
Picker(selection: seconds, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
.id(UUID().uuidString)
Text("Sec.")
Spacer()
}
}
You can solve it with this way:
if openSetting == .start {
TimePicker1(minutes: $startMinutes, seconds: $startSeconds)
}
.......
if openSetting == .end {
TimePicker1(minutes: $endMinutes, seconds: $endSeconds).id(1)
}
One with id, the other without id.
26-07-19
I'll update my code as I'm making progress watching the WWDC video's. My data model is:
struct Egg: Identifiable {
var id = UUID()
var thumbnailImage: String
var day: String
var date: String
var text: String
var imageDetail: String
var weight: Double
}
#if DEBUG
let testData = [
Egg(thumbnailImage: "Dag-1", day: "1.circle", date: "7 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-1", weight: 35.48),
Egg(thumbnailImage: "Dag-2", day: "2.circle", date: "8 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-2", weight: 35.23),
Egg(thumbnailImage: "Dag-3", day: "3.circle", date: "9 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-3", weight: 34.92)
Etc, etc
]
I've a TabbedView, a ContentView, a ContentDetail and a couple of other views (for settings etc). The code for the ContentView is:
struct ContentView : View {
var eggs : [Egg] = []
var body: some View {
NavigationView {
List(eggs) { egg in
EggCell(egg: egg)
}
.padding(.top, 10.0)
.navigationBarTitle(Text("Egg management"), displayMode: .inline)
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(eggs: testData)
}
}
#endif
struct EggCell : View {
let egg: Egg
var body: some View {
return NavigationLink(destination: ContentDetail(egg: egg)) {
ZStack {
HStack(spacing: 8.0) {
Image(egg.thumbnailImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.leading, -25)
.padding(.top, -15)
.padding(.bottom, -15)
.padding(.trailing, -25)
.frame(width: 85, height: 61)
VStack {
Image(systemName: egg.day)
.resizable()
.frame(width: 30, height: 22)
.padding(.leading, -82)
Spacer()
}
.padding(.leading)
VStack {
Text(egg.date)
.font(.headline)
.foregroundColor(Color.gray)
Text(egg.weight.clean)
.font(.title)
}
}
}
}
}
}
extension Double {
var clean: String {
return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(format: "%.2f", self)
}
}
The code for the ContentDetail is:
struct ContentDetail : View {
let egg: Egg
#State private var photo = true
#State private var calculated = false
#Binding var weight: Double
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(egg.date)
.font(.title)
.fontWeight(.medium)
.navigationBarTitle(Text(egg.date), displayMode: .inline)
ZStack (alignment: .topLeading) {
Image(photo ? egg.imageDetail : egg.thumbnailImage)
.resizable()
.aspectRatio(contentMode: .fill)
.background(Color.black)
.padding(.trailing, 0)
.tapAction { self.photo.toggle() }
VStack {
HStack {
Image(systemName: egg.day)
.resizable()
.padding(.leading, 10)
.padding(.top, 10)
.frame(width: 50, height: 36)
.foregroundColor(.white)
Spacer()
Image(systemName: photo ? "photo" : "wand.and.stars")
.resizable()
.padding(.trailing, 10)
.padding(.top, 10)
.frame(width: 50, height: 36)
.foregroundColor(.white)
}
Spacer()
HStack {
Image(systemName: "arrow.left.circle")
.resizable()
.padding(.leading, 10)
.padding(.bottom, 10)
.frame(width: 50, height: 50)
.foregroundColor(.white)
Spacer()
Image(systemName: "arrow.right.circle")
.resizable()
.padding(.trailing, 10)
.padding(.bottom, 10)
.frame(width: 50, height: 50)
.foregroundColor(.white)
}
}
}
Text("the weight is: \(egg.weight) gram")
.font(.headline)
.fontWeight(.bold)
ZStack {
RoundedRectangle(cornerRadius: 10)
.padding(.top, 45)
.padding(.bottom, 45)
.border(Color.gray, width: 5)
.opacity(0.1)
HStack {
Spacer()
DigitPicker(digitName: "tens", digit: $weight.tens)
DigitPicker(digitName: "ones", digit: $weight.ones)
Text(".")
.font(.largeTitle)
.fontWeight(.black)
.padding(.bottom, 10)
DigitPicker(digitName: "tenths", digit: $weight.tenths)
DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
Spacer()
}
}
Toggle(isOn: $calculated) {
Text(calculated ? "This weight is calculated." : "This weight is measured.")
}
Text(egg.text)
.lineLimit(2)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
Spacer()
}
.padding(6)
}
}
#if DEBUG
struct ContentDetail_Previews : PreviewProvider {
static var previews: some View {
NavigationView { ContentDetail(egg: testData[0]) }
}
}
#endif
struct DigitPicker: View {
var digitName: String
#Binding var digit: Int
var body: some View {
VStack {
Picker(selection: $digit, label: Text(digitName)) {
ForEach(0 ... 9) {
Text("\($0)").tag($0)
}
}.frame(width: 60, height: 110).clipped()
}
}
}
fileprivate extension Double {
var tens: Int {
get { sigFigs / 1000 }
set { replace(tens: newValue) }
}
var ones: Int {
get { (sigFigs / 100) % 10 }
set { replace(ones: newValue) }
}
var tenths: Int {
get { (sigFigs / 10) % 10 }
set { replace(tenths: newValue) }
}
var hundredths: Int {
get { sigFigs % 10 }
set { replace(hundredths: newValue) }
}
private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
self = Double(0
+ 1000 * (tens ?? self.tens)
+ 100 * (ones ?? self.ones)
+ 10 * (tenths ?? self.tenths)
+ (hundredths ?? self.hundredths)) / 100.0
}
private var sigFigs: Int {
return Int((self * 100).rounded(.toNearestOrEven))
}
}
The compiler errors I'm still getting are:
in ContentView, beneath NavigationLink: Missing argument for
parameter 'weight' in call
in ContentDetail, at NavigationView: Missing argument for parameter
'weight' in call
in ContentDetail, after #endif: Missing argument for parameter
'weight' in call
25-07-19
The following code is part of a List detail view. The var 'weight' is coming from the List through a 'NavigationLink' statement. In this code I declare it as '35.48', but the NavigationLink fills in its real value.
I want to make an array [3, 5, 4, 8] with the compactMap statement. That works okay in Playground. The values go to 4 different pickers (within a HStack).
import SwiftUI
import Foundation
struct ContentDetail : View {
var weight : Double = 35.48
var weightArray = "\(weight)".compactMap { Int("\($0)") }
#State var digit1 = weightArray[0] // error
#State var digit2 = weightArray[1] // error
#State var digit3 = weightArray[2] // error
#State var digit4 = weightArray[3] // error
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(weight)
.font(.title)
.fontWeight(.medium)
etc etc
I get an error 'Cannot use instance member 'weightArray' within property initializer; property initializers run before 'self' is available'.
If I use the following code it works fine (for the first list element):
import SwiftUI
import Foundation
struct ContentDetail : View {
var weight : Double = 35.48
var weightArray = [3, 5, 4, 8]
#State var digit1 = 3
#State var digit2 = 5
#State var digit3 = 4
#State var digit4 = 8
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(weight)
.font(.title)
.fontWeight(.medium)
etc etc
What is the correct SwiftUI approach and why?
Indeed, a property initializer cannot refer to another property in the same container. You have to initialize your properties in an init instead.
struct ContentDetail: View {
var weight: Double
var weightArray: [Int]
#State var digit1: Int
#State var digit2: Int
#State var digit3: Int
#State var digit4: Int
init(weight: Double) {
self.weight = weight
weightArray = "\(weight)".compactMap { Int("\($0)") }
_digit1 = .init(initialValue: weightArray[0])
_digit2 = .init(initialValue: weightArray[1])
_digit3 = .init(initialValue: weightArray[2])
_digit4 = .init(initialValue: weightArray[3])
}
But I suspect you're breaking out the digits because you want to let the user edit them individually, like this:
If that's what you want, you should not have a separate #State property for each digit. Instead, weight should be a #Binding and it should have a separate mutable property for each digit.
First, extend Double to give you access to the digits:
fileprivate extension Double {
var tens: Int {
get { sigFigs / 1000 }
set { replace(tens: newValue) }
}
var ones: Int {
get { (sigFigs / 100) % 10 }
set { replace(ones: newValue) }
}
var tenths: Int {
get { (sigFigs / 10) % 10 }
set { replace(tenths: newValue) }
}
var hundredths: Int {
get { sigFigs % 10 }
set { replace(hundredths: newValue) }
}
private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
self = Double(0
+ 1000 * (tens ?? self.tens)
+ 100 * (ones ?? self.ones)
+ 10 * (tenths ?? self.tenths)
+ (hundredths ?? self.hundredths)) / 100.0
}
private var sigFigs: Int {
return Int((self * 100).rounded(.toNearestOrEven))
}
}
Then, change ContentDetail's weight property to be a #Binding and get rid of the other properties:
struct ContentDetail: View {
#Binding var weight: Double
var body: some View {
HStack {
DigitPicker(digitName: "tens", digit: $weight.tens)
DigitPicker(digitName: "ones", digit: $weight.ones)
DigitPicker(digitName: "tenths", digit: $weight.tenths)
DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
}
}
}
struct DigitPicker: View {
var digitName: String
#Binding var digit: Int
var body: some View {
VStack {
Picker(selection: $digit, label: Text(digitName)) {
ForEach(0 ... 9) {
Text("\($0)").tag($0)
}
}.frame(width: 60, height: 110).clipped()
}
}
}
Here's the rest of the code needed to test this in a playground, which is how I generated that image above:
import PlaygroundSupport
struct TestView: View {
#State var weight: Double = 35.48
var body: some View {
VStack(spacing: 0) {
Text("Weight: \(weight)")
ContentDetail(weight: $weight)
.padding()
}
}
}
let host = UIHostingController(rootView: TestView())
host.preferredContentSize = .init(width: 320, height: 240)
PlaygroundPage.current.liveView = host