Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI WidgetKit: How to anchor views to the top

I am attempting to layout a tableView using SwiftUI and WidgetKit and would like to achieve a similar result of that as the Apple's Notes widget.

My current implementation succeeds in laying out the view in the .systemLarge widget, but not in the .systemMedium widget. I would like to pin the view to the top of the widget, such that the header of "FAVOURITES" is visible in the .systemMedium.

struct PlacesWidgetEntryView : View {
    var entry: Provider.Entry

    let places = [
        Place(name: "Place 1", imageName: "baseline_star_black_24pt"),
        Place(name: "Place 2", imageName: "baseline_star_black_24pt"),
        Place(name: "Place 3", imageName: "baseline_star_black_24pt"),
        Place(name: "Place 4", imageName: "baseline_star_black_24pt"),
        Place(name: "Place 5", imageName: "baseline_star_black_24pt"),

    ]
    
    var body: some View {
        VStack {
            //Header
            HStack {
                Text("FAVOURITES")
                    .bold()
                    .frame(height: 8)
                Spacer()
            }
            .padding()
            .background(Color.blue)

            //TableView
            LazyVStack {
                ForEach(places, id: \.self) { place in
                    PlaceRow(place: place)
                }
            }
            Spacer()
        }
    }
}

struct PlaceRow: View {
    let place: Place
    
    var body: some View {
        HStack {
            Text(place.name)
                .font(.body)
            Spacer()
            Image(place.imageName)
                .resizable()
                .frame(width: 28, height: 28, alignment: .center)
        }
        .padding(.horizontal)
        .padding(.vertical, 4)
    }
}

Implementation outcome:

enter image description here

The above is .systemLarge, which is good, and as per what I'm expecting.

enter image description here

The above is .systemMedium, which is not what I'm expecting. I would like to see "Favourites" anchored to the top of the widgetView, and potentially the tableView overflowing to the bottom.

like image 795
Koh Avatar asked Oct 10 '20 05:10

Koh


2 Answers

Here is possible layout solution. Tested with Xcode 12.

demo

var body: some View {
    VStack(spacing: 0) {
        HStack {
            Text("FAVOURITES")
                .bold()
                .frame(height: 8)
            Spacer()
        }
        .padding()
        .background(Color.blue)
        
        Color.clear
            .overlay(
                LazyVStack {
                    ForEach(places, id: \.self) { place in
                        PlaceRow(place: place)
                    }
                },
                alignment: .top)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
}

backup

like image 133
Asperi Avatar answered Sep 23 '22 15:09

Asperi


So from playing around in WidgetKit, it seems like if there are too many views in a widget, it starts to push the upper views off the screen. If you add more places to the array, you'll see the same thing happen with your large widget. What you can do is create separate views: one for the medium and one for the large widget, and for the medium one, just use 1-3 of your place objects to populate it.

You can use a switch statement in your PlacesEntryWidgetView with the widgetFamily to decide what you want to show on the view. I also slightly reduced the height of the image from 28 to 24.

struct PlacesWidgetEntryView : View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family
    
    let places = [
            Place(name: "Place 1", imageName: "blackStar"),
            Place(name: "Place 2", imageName: "blackStar"),
            Place(name: "Place 3", imageName: "blackStar"),
            Place(name: "Place 4", imageName: "blackStar"),
            Place(name: "Place 5", imageName: "blackStar"),
            Place(name: "Place 6", imageName: "blackStar"),
            Place(name: "Place 7", imageName: "blackStar"),
            Place(name: "Place 8", imageName: "blackStar")
        ]

    @ViewBuilder
    var body: some View {
        switch family {
        case .systemMedium:
            // widget can only show so many views so I only took first 3 places
            WidgetView(places: Array(places.prefix(3)))
        case .systemLarge:
            WidgetView(places: places)
        // I only have it set to show system medium so you can ignore
        // the last case
        case .systemSmall:
            Text("")
        @unknown default:
            Text("")
        }
    }
}

struct WidgetView: View {
    let places: [Place]
    
    var body: some View {
        VStack {
            //Header
            HStack {
                Text("FAVOURITES")
                    .bold()
                    .frame(height: 8)
                Spacer()
            }
            .padding()
            .background(Color.blue)

            //TableView
            LazyVStack {
                ForEach(places, id: \.self) { place in
                    PlaceRow(place: place)
                }
            }
            Spacer()
        }
    }
}

struct PlaceRow: View {
    let place: Place
    
    var body: some View {
        HStack {
            Text(place.name)
                .font(.body)
            Spacer()
            Image(place.imageName)
                .resizable()
                .frame(width: 28, height: 24, alignment: .center)
        }
        .padding(.horizontal)
        .padding(.vertical, 4)
    }
}

This is the preview:

enter image description here

Keep in mind, that you might want to switch between the simulators to make sure your widgets look good on all devices.

like image 24
droberson Avatar answered Sep 26 '22 15:09

droberson