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)
Post
Replies
Boosts
Views
Activity
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)
}
}
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.
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, 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...
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 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.
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?
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?
Aah, sorry about this. Looking again after your comment I realised I broke your StepperField by adding a "spacing: 0" to the H-Stack.I did this much earlier in my code to get the TextField and Stepper to be adjacent one another, as it normally appears.I've modified the StepperField to put the TextField and Stepper in their own HStack, so that the spacing between them can be removed. In the code below, I used to have a spacing of 0 on line 7, but now added it at line 9.struct StepperField: View {
let title: LocalizedStringKey
@Binding var value: Int
var alignToControl: Bool = false
var body: some View {
HStack {
Text(title)
HStack(spacing: 0) {
TextField("Enter Value", value: $value, formatter: NumberFormatter())
.multilineTextAlignment(.center)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(minWidth: 15, maxWidth: 60)
.alignmentGuide(.controlAlignment) { $0[.leading] }
Stepper(title, value: $value, in: 1...100)
.labelsHidden()
}
}
.alignmentGuide(.leading) {
self.alignToControl
? $0[.controlAlignment]
: $0[.leading]
}
}
}
Thanks, that helps a lot, and I agree about the rabit hole.A stepper with a label and text edit is such a standard thing that I hoped to just be missing it when posting the question. I tried to find other examples of steppers being used inside of apples own applications, and could not come up with any.I am afraid this is still not 100% correct, but I have enough to make it work, but it is an ugly hack, and will almost certainly break in future macos releases. If you look carefully in my example, then you will see that the two labels (Text) do not right-align correctly. Your alignment does make the content correctly left alight, but not the labels.My work-around was to add trailing padding to the Text, but then it is too much. I had to change the actual padding value to get it just-right, and that meant pixel counting.I am afraid without knowing how apple does alignment in forms, there is probably not going to be a way to get it perfect.Thanks again for the help.
Thanks, that helps a lot, but I'm still struggling with the alignment.I am on macOS, and my control is wrapped in a Form, so that it aligns controls as expected (eg. see XCode Text Editing preferences form).The following bit of code in my form:HStack(spacing: 0) {
Text("Count:")
TextField("", value: $numDigits, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
.frame(minWidth: 15, maxWidth: 25)
Stepper("Bla", value: self.$numDigits, in: 0...9)
.labelsHidden()
}gives something looking like this:I would like "Count:" and "Format:" to line up, right justified.I've played around trying to add spacers, dividers, different modifiers, etc. but all come out wrong.Any ideas?