That is strange. With Xcode 11.4 and macOS 10.15.4 I am getting different results with the if statement (line 12) commented out.Below is a screen-shot showing the app running with the line commented out.Either way, it should work with or without that if statement. If you're result is that it does not work, then the question is still, why?
Post
Replies
Boosts
Views
Activity
Hmm, that's interesting. I can confirm adding it to another HStack fixes the issue.This looks like a bug, or am I missing something?
Thanks for the response.I've looked at the link you shared, and it does not give me more insight.I could be just really confused, or explaining really badly.animatableData does not return a reference to a property, it only returns a value. As shown in my second example, it uses scaleAsDouble, which is not connected to scale, other than the logic of my code.What if I had another property? What bugs me, is how the framework knows which property it has to interpolate. I've added prints to the getters and setters of scaleAsDoulbe when changing scale, with a .animation modifier.What I DO know: The framework knows about the instance of MyRect. When I change the scale property, it creates another instance of MyRect, and init() is called with this new value. Let's say scale was 3 on the first instance, and 4 on the second instance. The framework now knows that it must call the setter on animatableData with the interpolated values between 3 and 4. After calling the setter, it calls path() to get the new interpolated path. But if I had two properties, and both were changed, then how does the framework know that I am animating scale, and not the other property?What I also did in order to try and understand was to hard-code a value in animatableData that is not the same as the property value. I could see that the framework always calls the getter on animatableData before doing anything else. When the value returned by animatableData was not the same as my property's value, then it would not animate, and the setter would never be called.From this, it looked to me as if the framework was checking which value is returned by animatableData, and matching that to one of the properties of my struct so that it knew which one to animate, but that does not make sense. If two properties had the same starting value, then this would not work, plus it just sounds silly.I hope this helps explain what I am trying to figure out. I am sure I am missing something streight forward, but cannot figure it out.To answer your question about the code that does the animation, this is my test code to animate MyRect:struct ScaledRectView: View {
@State private var scale: Int = 100
let possibleScales: [Int] = [10, 20, 50, 100]
var body: some View {
VStack {
MyRect(scale: self.scale)
.stroke(Color.blue, lineWidth: 4)
.animation(.easeInOut(duration: 1.0))
Text(verbatim: "Scale: \(scale) %")
HStack {
ForEach(possibleScales, id: \.self) { scale in
Button(action: {
self.scale = scale
}, label: { Text("\(scale) %") })
}
}
}
.padding()
}
}As you can see, this changes the scale as Integers, but the underlying logic (through animatableData) uses a different value that is of type Double.
Nope, sorry, that does not make a difference. Adding the || (item.id == 0) means that the first item will always slide up. This does not solve it for two reasons: 1) The first item is not always the top, depending on how far you went down the list; 2) It must not always go in the same direction.You'rs probably looked like it worked because you only went down once, then up again. Have a look at my PreviewProvider. It generated more itmes so that I could go further down the list before going up again.The problem remains: The .transition, with its parameters is added to the Text when it is inserted, or modified. It therefore has the state of the direction at that point. If is is removed because of a change of direction, then it will slide in the wrong direction.I am wondering if there is a way to reset the animation and/or transition when the state changes or delaying it somehow so that the body is re-calculated when the direction changes, and then changed. Any ideas?
Thanks, this does solve the problem, but it is not as elegant as I have hoped. I am always concerned when I see arbitary timeouts in source code (the 0.1 delay in dispatch - you can change it from 100 ms to 1 ms and it still works). API changes, or even change in CPU load can change the code's behaviour.But it does solve the problem, and also clearly illustrates what is going on, so I've marked it as correct. It would be nice though if there was a way to change the transision in a more deterministic way. I may keep on playing with this one for a while...
How about this one?The main difference is that I do not have a timed async call. I am trying to more accuritly show what is happening, and trying to solve it.They key issue is that the .transision on the text must be changed when the direction is changed before the text is removed. I did this by changing slideUp to currSlideDirection and adding another state, updatePending. If you press a button to slide up or down, it checks the current direction. If the current direction is not correct, then it only changes the current direction, and set the updatePending flag to indicate that a slide update is pending.When the body is re-calculated (with the correct slide direction being aplied), the code checks if updatePending is set. If so, it clears updatePending, and slides in the current direction. It does this asynchronously (but as soon as it can, without timeout) simply because you cannot change state when the body is being re-calculated.I've often run up against the issue of not being able to change state when the body of a view is being calculated, and had to solve it with more complex solutions. This patern of saving a state to indicate that an update is pending, and then doing it asynchronously may be a clean work around for many other similar problems.struct ContentView: View {
private struct DisplayItem {
var id: Int
var text: String
}
private enum SlideDirection {
case up
case down
}
private let numVisibleItems = 3
@State private var currSlideDirection: SlideDirection = .down
@State private var updatePending: Bool = false
@State private var startIndex: Int = 0
// var items: [String] = []
var items: [String] = ["Hello", "You", "Boys", "Girls"]
var body: some View {
if updatePending {
// updatePending is set to true when the slide direction changed, but the
// slide has not been implemented yet. Change the state (startIndex)
// asynchronously so that the state is not changed in the body of the
// view:
DispatchQueue.main.async {
self.updatePending = false
self.currSlideDirection == .up ? self.slideUp() : self.slideDown()
}
}
return HStack {
VStack {
ForEach(visibleItems(), id: \DisplayItem.id) { item in
Text("\(item.text)").font(.system(size: 30)).animation(.easeInOut)
.transition(.asymmetric(
insertion: AnyTransition.opacity.combined(with: .move(edge: self.currSlideDirection == .down ? .top : .bottom )),
removal: AnyTransition.opacity.combined(with: .move(edge: self.currSlideDirection == .up ? .top : .bottom ))))
}
}
.animation(.easeInOut(duration: 1.0))
.frame(width: 200, height: 200)
VStack {
Button(action: {
if self.currSlideDirection == .down {
// The current slide direction needs to change. This changes the
// state, without sliding yet. The state change will change the
// transistion on the text above but the slide will only happen
// after this.
self.updatePending = true
self.currSlideDirection = .up
} else {
// Since we are already sliding in the correct direction, we do not
// have to update the transition on the text above, so we can just
// slide.
self.slideUp()
}
}, label: { Text("Slide Up") })
Button(action: {
if self.currSlideDirection == .up {
// See comment at slide up above.
self.updatePending = true
self.currSlideDirection = .down
} else {
// See comment at slide up above.
self.slideDown()
}
}, label: { Text("Slide Down") })
}
}
}
func slideUp() {
self.startIndex = min(self.items.count-1, self.startIndex + 1)
}
func slideDown() {
self.startIndex = max(0, self.startIndex - 1)
}
private func visibleItems() -> [DisplayItem] {
let endIndex = min(startIndex + numVisibleItems - 1, items.count - 1)
var result = [DisplayItem]()
for i in startIndex...endIndex {
result.append(DisplayItem(id: i, text: items[i]))
}
return result
}
}
Thanks, I needed to go up the device tree to find the property that I needed.Sorry, by IOService utility app, I meant IORegistryExplorer. It is one of the XCode developer utilities. It is a GUI version of ioreg. It has good search and filter functions making it much easier to find what you are looking for.
That is brilliant, thanks, and scales nicely for various different types.It does have a slight side-effect thought that I have not been able to get around. I also cannot see that it is possible to get around.Take the slightly different example below where MyStruct does not contain Ints, but Doubles. When you start off, the text will be "1.2 3.4". As soon as you delete the "4", the text does not change to "1.2 3." as a user would expect, but to "1.2 3.0" because "3." is converted to a valid double.Any ideas of how to get around it?struct MyStruct {
var a: Double
var b: Double
}
extension MyStruct {
var strValue: String {
get {
return "\(a) \(b)"
} set {
let parts = newValue.components(separatedBy: " ")
guard parts.count == 2, let aValue = Double(parts[0]), let bValue = Double(parts[1]) else {
// Not valid.
return
}
a = aValue
b = bValue
}
}
}
struct MyStructView: View {
@Binding var value: MyStruct
var body: some View {
TextField("", text: $value.strValue)
}
}
struct ContentView: View {
@State var myStruct = MyStruct(a: 1.2, b: 3.4)
var body: some View {
return VStack {
Text("xValue = \(myStruct.a)")
Text("yValue = \(myStruct.b)")
Divider()
MyStructView(value: $myStruct)
} .padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Hi OOPer,Below is my quickly hacked code to handle rotations. You could use any model that publishes isPortrait to use it as the environmental object.class AppDelegate: UIResponder, ObservableObject, UIApplicationDelegate {
@Published var isPotrait = true
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
return true
}
@objc func rotated() {
self.isPotrait = UIDevice.current.orientation.isPortrait
print("isPortrait = \(isPotrait)")
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
}You also need to add the following (or something similar) to the SceneDelegate where the ContentView is instantiated:// Create the SwiftUI view that provides the window contents.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let contentView = ContentView().environmentObject(appDelegate)
I found a much better solution. Not only does it solve the original problem, but it also solves the problem when editing doubles (see previous comments) and it allows a single value to be edited by multiple TextFields (or sliders, or any other visual element).
The key is in not binding the TextField to a string, but creating a new Binding with a getter and setter. A local private string is used to keep the string being edited by the TextField in synch with the value (or partial value in this case).
The example below shows a struct with two members. A value of this type can be passed to the custom view that is used to edit it. The value is shown at the top of the view (for information only). The view has two TextFields that is used to edit the two members of the value.
This is just a proof of concept and can be improved further by using Formatters. This example also scales nicely in that a custom view can be created to edit a custom type and the complexity can be hidden inside the custom view.
struct MyStruct {
	var a: Int
	var b: Double
}
struct MyStructView: View {
	@Binding var myStruct: MyStruct
	
	@State private var lastValidValue = MyStruct(a: 10, b: 20)
	@State private var isEditingA = false
	@State private var isEditingB = false
	@State private var aStrValue = ""
	@State private var bStrValue = ""
	
	var body: some View {
		
		VStack {
			Text("myStruct.a = \(myStruct.a)	.b = \(myStruct.b)")
			Divider()
			
			HStack {
				Text("a = ")
				TextField("Value a", text: Binding(
				get: {
					if self.isEditingA {
						return self.aStrValue
					} else {
						return "\(self.myStruct.a)"
					}
				}, set: { str in
					self.aStrValue = str
					
					if let tmp = Int(str) {
						self.myStruct.a = tmp
						self.lastValidValue = self.myStruct
					} else {
						self.myStruct.a = self.lastValidValue.a
					}
				}), onEditingChanged: { editing in
					self.isEditingA = editing
				})
			}
			
			HStack {
				Text("b = ")
				TextField("Value b", text: Binding(
					get: {
						if self.isEditingB {
							return self.bStrValue
						} else {
							return "\(self.myStruct.b)"
						}
				}, set: { str in
					self.bStrValue = str
					
					if let tmp = Double(str) {
						self.myStruct.b = tmp
						self.lastValidValue = self.myStruct
					} else {
						self.myStruct.b = self.lastValidValue.b
					}
				}), onEditingChanged: { editing in
					self.isEditingB = editing
				})
			}
		}
		.frame(width: 300, height: 100)
		.padding()
		.onAppear {
			self.lastValidValue = self.myStruct
		}
	}
}
struct ContentView: View {
	@State private var myStruct = MyStruct(a: 1, b: 2.0)
	
	var body: some View {
		MyStructView(myStruct: $myStruct)
	}
}
struct ContentView2_Previews: PreviewProvider {
	static var previews: some View {
		ContentView()
	}
}
I've also asked this question (slightly differently) months ago and managed to solve it.
See the response to my question here. - https://developer.apple.com/forums/thread/124617
I found that changing my picker to use an Enum works:
Picker("Pick Something: ", selection: $pickedValue) {
	ForEach(TestState.allCases) { state in
		Text(state.rawValue).tag(state)
	}
}
Why would this work differently from the initial code?
TestState is declared:
enum TestState: String, CaseIterable, Identifiable {
case one
case two
var id: String { self.rawValue }
}
No, I have not.
I found a workaround by wrapping a NSTextField in a NSViewRepresentable but this broke again in Big Sur. So, I am again trying to resolve this.
ps. Sorry for only replying now. For some reason I missed the notification.
My app is displaying a default value as the user enters data in the TextField. It is clear that this data that is being displayed will be copied.
The problem does not seem to be related to Enabled since it is set in IB. I got it to work by removing the view that had focus (in my case a TextField). When I did this, Copy was enabled and my copy function in code above was called.
My problem still seems to be related to providing and telling the system that copy is available when the current first responder cannot handle it.
I've tried various things like creating my own NSTextField and trying to override copy but without success. I've also created a new SwiftUI app and posted this thread - https://developer.apple.com/forums/thread/667633 since it more clearly shows my issue.
ps. Sorry for the late reply. It seems my notifications are not working.
Thanks, that did help.
For completeness, I wrote this small sample app that adds a new menu item to the menu and overrides the copy menu.
@main
struct MenuTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("My Top Menu") {
Button("Sub Menu Item") { print("You pressed sub menu.") }
.keyboardShortcut("S")
}
CommandGroup(replacing: .pasteboard) {
Button("Cut") { print("Cutting something...") }
.keyboardShortcut("X")
Button("Copy") { print("Copying something...") }
.keyboardShortcut("C")
Button("Paste") { print("Pasting something...") }
.keyboardShortcut("V")
Button("Paste and Match Style") { print("Pasting and Matching something...") }
.keyboardShortcut("V", modifiers: [.command, .option, .shift])
Button("Delete") { print("Deleting something...") }
.keyboardShortcut(.delete)
Button("Select All") { print("Selecting something...") }
.keyboardShortcut("A")
}
}
}
}