Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usability issues #14

Open
bgbernovici opened this issue Feb 23, 2024 · 6 comments
Open

Usability issues #14

bgbernovici opened this issue Feb 23, 2024 · 6 comments

Comments

@bgbernovici
Copy link

bgbernovici commented Feb 23, 2024

Hi, thank you for sharing these samples. They have been immensely helpful in getting me started with Swift and WinRT. However, I seem to be encountering a few challenges, and I was hoping to get some clarification on certain aspects. Specifically:

  • I cannot get the navigation to work using the Frame class. Essentially I create a root frame, I set the frame as the content of the NavigationView, but I cannot use Swift's type(of: page) as a parameter, because instead it is expecting TypeName. I have tried mocking it like: TypeName(name: "Microsoft.UI.Xaml.Controls.Page", kind: TypeKind.primitive), but that is generic and I want to point it to my derived page, though I am not sure how to achieve that.
  • Can a static resource be represented and named in code without setting it at the Application level with XAML? I will share an example below:
lazy var pointerEnteredGradientBrush = {
        var brush = LinearGradientBrush()
        var redGradientStop = GradientStop()
        redGradientStop.color = Color(a:255,r:42,g:13,b:52)
        redGradientStop.offset = 0.0
        lazy var blueGradientStop = GradientStop()
        blueGradientStop.color = Color(a:255,r:42,g:255,b:255)
        blueGradientStop.offset = 1.0

        brush.gradientStops.append(redGradientStop)
        brush.gradientStops.append(blueGradientStop)
        return brush
}()

Application.current.resources.insert("pointerEnteredGradientBrush", pointerEnteredGradientBrush)

lazy var controlTemplate = XamlReader.load("""
        <ControlTemplate TargetType="GridViewItem"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
            <GridViewItemPresenter PointerOverBackground="{StaticResource pointerEnteredGradientBrush}"/>
        </ControlTemplate>
""")
lazy var itemContainerStyle = {
        var style = Style()
        style.targetType = TypeName(name: "Microsoft.UI.Xaml.Controls.GridViewItem", kind: TypeKind.primitive)
        style.setters.append(Setter(GridViewItem.templateProperty, controlTemplate))
        return style
}()
  • I'm struggling to update UI values upon data change events. I have read about how state and data binding works here, but I cannot get it to work. I've tried deriving my class from DependencyObject and I've also tried implementing INotifyPropertyChanged. With the latter the issue is that I do not know exactly how to replace the event delegate that is used in C#, my class looks something like that, but propertyChanged only gives me the addHandler and removeHandler which I do not know exactly how should they be used in tandem with didSet. In C# it seems pretty straightforward how it works by invoking it for each member:
public class Vehicle: INotifyPropertyChanged  {

  var name: String

  public var propertyChanged: Event<PropertyChangedEventHandler> = Event(
          add: { _ in return EventRegistrationToken() },
          remove: { _ in }
      )
   ....
}

And then I assume that the data binding on the TextBlock control should work in the following way? Or should it be defined like a data source for the item container?

    lazy var nameTextBlock: TextBlock =  {
        let name = TextBlock()
        name.textWrapping = .wrap
        name.textTrimming = .wordEllipsis
        name.fontWeight = FontWeights.bold
        name.textDecorations = TextDecorations.underline
        name.fontSize = 14
        name.maxHeight = 20
        name.foreground = SolidColorBrush(Color(a:255, r:255,  g:255,  b:255))

        var b: Binding = Binding()
        b.mode = BindingMode.oneWay
        b.source = vehicle.name
        try! name.setBinding(TextBlock.textProperty, b)

        return name
    }()

Thank you,
Bogdan

@paytontech
Copy link
Contributor

I'm also unable to navigate via frame!

@bgbernovici
Copy link
Author

bgbernovici commented Feb 26, 2024

I gave it another try this evening. After reading about property wrappers, I ended up with this:

public class Vehicle: INotifyPropertyChanged  {

    private var names: String
    public var namesHandler: String {
        get {return self.names}
        set {
            self.names = newValue
            self._propertyChanged.invoke(self, PropertyChangedEventArgs("namesHandler"))
        }
    }

    @EventSource<PropertyChangedEventHandler> public var propertyChanged: Event<PropertyChangedEventHandler>
}

And the binding is:

var b: Binding = Binding()
b.mode = BindingMode,oneWay
b.source = container.namesHandler
b.updateSourceTrigger = .propertyChanged
try! names.setBinding(TextBlock.textProperty, b)

But it still does not update the TextBlock. I've also tried using PropertyPath after setting the DataContext without any luck. I assume that it stems from the fact that my variables in the Swift class are not bindable.

@ducaale
Copy link

ducaale commented Mar 4, 2024

I would also like to know if there is a way to do navigation using Frame and Page classes.

By the way, you can use Swift's Observation Framework as an alternative to data binding. For example, given the following button that tracks changes to its content:

Expand for a cybernetically enhanced button + helpers
extension Button {
    convenience init(
        content: @autoclosure @escaping () -> String,
        onClick: (() -> ())? = nil
    ) {
        self.init()
        self.updateContent(content)
        if let onClick = onClick {
            self.click.addHandler { _, _ in onClick() }
        }
    }

    func updateContent(_ content: @escaping () -> String) {
        withObservationTracking {
            self.content = content()
        } onChange: {
            // We need to defer updating content since this closure is executed **before** the observed state
            // has been updated.
            //
            // Also, `onChange` is invoked once for each `withObservationTracking` call so we need to
            // recursively call `updateContent`.
            guard let dispatcherQueue = WinAppSDK.DispatcherQueue.getForCurrentThread() else { return }
            let _ = try! dispatcherQueue.tryEnqueue {
                self.updateContent(content)
            }
        }
    }
}

// -------------- More helpers for SwiftUI-like experience -------------
extension StackPanel {
    convenience init(content: () -> UIElement) {
        self.init()
        self.children.append(content())
    }
}

extension Window {
    convenience init(content: (() -> UIElement)? = nil) {
        self.init()
        if let content = content {
            self.content = content()
        }
    }
}

extension FrameworkElement {
    func verticalAlignment(_ alignment: VerticalAlignment) -> Self {
        self.verticalAlignment = alignment
        return self
    }

    func horizontalAlignment(_ alignment: HorizontalAlignment) -> Self {
        self.horizontalAlignment = alignment
        return self
    }
}

You can use it like this:

import Foundation
import UWP
import WinAppSDK
import WindowsFoundation
import WinUI
import Observation

@Observable class State {
    var counter: Int = 0
}

@main
public class PreviewApp: SwiftApplication {
    var state = State()

    lazy var window = Window {
        StackPanel {
            Button(content: "clicked \(self.state.counter) times") {
                self.state.counter += 1
            }
        }
        .horizontalAlignment(.center)
        .verticalAlignment(.center)
    }

    override public func onLaunched(_ args: WinUI.LaunchActivatedEventArgs) {
        try! window.activate()
    }
}

@bgbernovici
Copy link
Author

Thank you for the suggestion @ducaale. I will definitely try it out.

@vetledv
Copy link

vetledv commented Mar 27, 2024

Hey, I am also struggling with navigation using Frame. Did anyone figure this out? 😄

@litewrap
Copy link

Fantastic work, thanks to @ducaale your suggestion work like a charm !
Looking for more examples with helpers for SwiftUI-like experience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants