SwiftUI WidgetKit: How to anchor views to the top - ios

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:
The above is .systemLarge, which is good, and as per what I'm expecting.
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.

Here is possible layout solution. Tested with Xcode 12.
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)
}

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:
Keep in mind, that you might want to switch between the simulators to make sure your widgets look good on all devices.

Related

How to navigate views with enums

I have followed a tutorial in SWIFT UI (only just started using it) and I am trying to open new views using the same logic applied so far. Basically there is a tab bar with 5 views (Search ,home etc...) which works opening a new view with each tabbar item, however in my homeview page I have some button cards that I want to open a new view. I can get the text for selectedSection to work but it shows the Text over the top of the homeview. How can I get it to open a new view entirely?
Here is my content view:
struct ContentView: View {
#AppStorage("selectedTab") var selectedTab: Tab = .home
#AppStorage("selectedSection") var selectedSection: Features = .calculators
#State var isOpen = false
#State var show = false
let button = RiveViewModel(fileName: "menu_button", stateMachineName: "State
Machine", autoPlay: false)
var body: some View {
ZStack {
Color("Background 2").ignoresSafeArea()
SideMenu()
.opacity(isOpen ? 1 : 0)
.offset(x: isOpen ? 0 : -300)
.rotation3DEffect(.degrees(isOpen ? 0 : 30), axis: (x: 0, y: 1, z: 0))
Group{
switch selectedTab {
case .home:
HomeView()
case .search:
Text("Search")
case .star:
Text("Favorites")
case .bell:
Text("Bell")
case .user:
Text("User")
}
switch selectedSection {
case .calculators:
Text("Calculators")
case .projects:
Text("Projects")
case .kvFinder:
Text("kv Finder")
}
}
And my home view:
var content: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Welcome")
.customFont(.largeTitle)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(sections) { section in
Button {
selectedSection = section.features
} label : {
VCard(section: section)
}
}
}
And here is my VCard:
struct Section: Identifiable {
var id = UUID()
var title: String
var subtitle: String
var caption: String
var color: Color
var image: Image
var features: Features
}
var sections = [
Section(title: "TAB Calculations", subtitle: "Find all basic and advanced HVAC
calculations", caption: "3 sections - over 40 calculators", color: Color(hex:
"7850F0"), image: Image(systemName: "x.squareroot"), features: .calculators),
Section(title: "Upcoming Projects", subtitle: "Find upcoming and current
commissioning projects.", caption: "Over 150 projects", color: Color(hex: "6792FF"),
image: Image(systemName: "folder.fill.badge.plus"), features: .projects),
Section(title: "Valve Kv Finder", subtitle: "Quickly determine valve flow rates from
brands such as Oventropp, IMI TA and Danfoss", caption: "150 tables", color:
Color(hex: "005FE7"), image: Image(systemName: "magnifyingglass"), features:
.kvFinder)
]
enum Features: String {
case calculators
case projects
case kvFinder
}
You can use NavigationStack API if your minimum app deployment is 16+. otherwise, you may use the old NavigationView.
You can find the migration document here.
struct ContentView: View {
#State var path: [YourDestinations] = []
var body: some View {
TabView {
VStack {
NavigationStack(path: $path) { // <= here
VStack {
NavigationLink("Card 1", value: YourDestinations.place1)
NavigationLink("Card 1", value: YourDestinations.place2)
NavigationLink("Card 1", value: YourDestinations.place3)
}
.navigationDestination(for: YourDestinations.self) { destination in
switch destination {
case .place1:
Text("Detination 1")
.foregroundColor(.yellow)
case .place2:
Text("Detination 2")
.foregroundColor(.green)
case .place3:
Text("Detination 3")
.foregroundColor(.gray)
}
}
}
}
.tabItem({
Text("Tab 1")
})
Text("Hello, world!")
.padding()
.tabItem({
Text("Tab 2")
})
Text("Hello, world!")
.padding()
.tabItem({
Text("Tab 3")
})
}
}
}

Multiple vertical ScrollViews in one SwiftUI view

I am trying to implement a tab-layout LazyVGrid that will contain three different data types. For this, I have taken a Single scrollView and have created multiple LazyVGrid to accommodate this data.
The problem I am facing is, that whenever a list from tab 1 is scrolled, the list from the tab scrolls at the same offset.
There are two different solutions I have already tried -
Create LazyVGrid as a variable - I was unable to do that since the data it will have belongs to ViewModel and also LazyVGrid is a separate view in a practical example.
Use ScrollView each time - I tried doing it but every time the currently selected type changes, SwiftUI forces LazyVGrid to repopulate and shows from offset 0.
Below is what my code looks like and it'd be great if someone could help me realize how I can fix this.
Please find my code and current output. I expect that when I switch tabs and come back, the ScrollView offset remains at the position where I left it.
import SwiftUI
enum SearchType: String, CaseIterable {
case movie = "Movies"
case tv = "TV Shows"
case people = "People"
}
struct SearchContainerView: View {
#ObservedObject var viewModel = SearchViewModel()
#State var currentSelectedSearchType: SearchType = .movie
let array: [String] = ["The", "above", "works", "great", "when", "you", "know", "where", "in", "the", "array", "the", "value" ,"is", "that", "is", "when" ,"you", "know", "its", "index", "value", "As", "the", "index", "values", "begin", "at" ,"0", "the" ,"second", "entry", "will", "be", "at", "index", "1"]
var body: some View {
NavigationView {
VStack {
HStack {
ForEach(SearchType.allCases, id: \.self) { type in
HStack {
Spacer()
Text(type.rawValue)
.font(.title3)
.onTapGesture {
self.currentSelectedSearchType = type
}
Spacer()
}
.padding(5)
.background(currentSelectedSearchType == type ? Color.gray : Color.clear)
.cornerRadius(10)
}
.background(Color.gray.opacity(0.5))
.cornerRadius(10)
.padding(.horizontal)
}
ScrollView {
switch currentSelectedSearchType {
case .movie:
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(array, id: \.self) {
Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center)
}
})
case .tv:
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(array, id: \.self) {
Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center)
}
})
case .people:
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(array, id: \.self) {
Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center)
}
})
}
}
}
}
}
}
Current Output -
<img src="https://i.imgur.com/TgkIXYo.mp4" alt="this slowpoke moves" width="250" />
I understand you want to switch between the different GridViews, but they should keep their individual scroll position.
To achieve that all 3 ScollViews have to stay in the view hierarchy, otherwise – as you stated – they are rebuilt and loose their position.
You can e.g. do that by putting all in a ZStack and controlling opacity (and activity) based on selection:
struct ContentView: View {
//#ObservedObject var viewModel = SearchViewModel()
#State var currentSelectedSearchType: SearchType = .movie
var body: some View {
NavigationView {
VStack {
HStack {
ForEach(SearchType.allCases, id: \.self) { type in
HStack {
Spacer()
Text(type.rawValue)
.font(.title3)
.onTapGesture {
self.currentSelectedSearchType = type
}
Spacer()
}
.padding(5)
.background(currentSelectedSearchType == type ? Color.gray : Color.gray.opacity(0.5))
.cornerRadius(10)
}
.padding(.horizontal)
}
ZStack { // here
SearchResults(type: "Movie")
.opacity(currentSelectedSearchType == .movie ? 1 : 0)
SearchResults(type: "Show")
.opacity(currentSelectedSearchType == .tv ? 1 : 0)
SearchResults(type: "Actor")
.opacity(currentSelectedSearchType == .people ? 1 : 0)
}
}
}
}
}
struct SearchResults: View {
let type: String
var body: some View {
ScrollView {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(0..<30) {
Text("\(type) \($0)")
.font(.title).bold()
.frame(height: 100, alignment: .center)
}
})
}
}
}

Swift UI navigationLink from item

I have problem with making this itemView to navigationLink. I need onTapGesture to open next list
https://github.com/reddogwow/test/blob/main/MainMenu
var objectView: some View {
VStack {
Text(objectname)
.foregroundColor(.white)
.font(.system(size: 25, weight: .medium, design: .rounded))
Image(objectphoto)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
}
.frame(height: 200)
.frame(maxWidth: .infinity)
.background(Color.blue)
}
Best edit will be where i can use Destination name from item (navMenu string)
I need something like this
var body: some View {
// NavigationView {
let columns = Array(
repeating: GridItem(.flexible(), spacing: spacing),
count: numbersOfColumns)
ScrollView {
HStack {
personView
petView
}
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(items) { item in
NavigationLink(destination: item.navMenu) {
Text("")
} label: {
ItemView(item: item)
}
}
}
.padding(.horizontal)
}
.background(Color.blue.ignoresSafeArea())
.navigationTitle("")
// }
}
Where line NavigationLink(destination: HERE MUST BE STRING TO navMenu) But now im in cycle lot of fails
I have some menus called
Menu1.swift
Menu2.swift
Menu3.swift
I need open this menu after click on Grid menu.
But destination: Must be filled with name from item in code.
struct item: Identifiable {
let id = UUID()
let title: String
let image: String
let imgColor: Color
let navMenu : String
}
item(title: "Menu 1", image: "img1", imgColor: .orange, navMenu: "Menu1"),
I thing I have bad written buy maybe only small mistake
or maybe make it like this?
var navMenuDest = destination: + item.navMenu
this will be
NavigationLink(navMenuDest) {
in finale looks like
NavigationLink(destination: Menu1)
You must have a NavigationView in the hierarchy to use NavigationLink. To make each ItemView navigate to a new view when tapped, we use NavigationLink as shown below.
Code:
struct MainMenu: View {
/* ... */
var body: some View {
NavigationView {
let columns = Array(
repeating: GridItem(.flexible(), spacing: spacing),
count: numbersOfColumns)
ScrollView {
HStack {
personView
objectView
}
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(items) { item in
NavigationLink {
Text("Some destination view here...\n\nItem: \(String(describing: item))")
} label: {
ItemView(item: item)
}
}
}
.padding(.horizontal)
}
.background(Color.blue.ignoresSafeArea())
.navigationTitle("Main Menu")
}
}
}
Result:

SwiftUI DisclosureGroup overlaps when expanded

I have a grid of DisclosureGroup enclosed in a 2 column LazyVGrid. The problem I am having is that when I expand the DisclosureGroup it overlaps the Text below the group. Is there a way to fix the overlap? Ideally, I would like the DisclosureGroupView (shown below) to push everything down.
I tried to use zIndex on the DisclosureGroup but it does not look good. It basically expands the entire grid which encloses the DisclosureGroup view.
Appreciate very much for any direction to fix this issue!
Here is what I have so far:
DisclosureGroupView:
struct DisclosureGroupView: View {
var groups: [String: [String]]
#State private var isExpanded: [String: Bool]
#State private var selectedItems = [String: Set<String>]()
#State private var isTitleChecked: [String: Bool]
init(groups: [String: [String]]) {
self.groups = groups
_isExpanded = State(initialValue: groups.reduce(into: [String: Bool](), { result, group in
result[group.key] = false
}))
_isTitleChecked = State(initialValue: groups.reduce(into: [String: Bool](), {result, group in
result[group.key] = false
}))
}
var body: some View {
GeometryReader { geo in
let ncols = 2
let width = geo.size.width / CGFloat(ncols)
let layout = Array(repeating: GridItem(.fixed(width), spacing: 5), count: ncols)
LazyVGrid(columns: layout, alignment: .leading) {
ForEach(groups.keys.sorted(), id: \.self) { title in
self.disclosureGroup(title: title, items: groups[title, default: [String]()])
}
}
}
}
private func disclosureGroup(title: String, items: [String]) -> some View {
let expanded = Binding<Bool>(get: {isExpanded[title]!}, set: {isExpanded[title] = $0})
return DisclosureGroup(title, isExpanded: expanded) {
ScrollView {
VStack(alignment: .leading) {
ForEach(Array(items.enumerated()), id: \.1.hashValue) { index, item in
Text(item)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.zIndex(1.0)
}
.frame(height: 60)
}
.font(.body)
.frame(width: 110)
.accentColor(.white)
.foregroundColor(.white)
.padding(5)
.background(Color(UIColor.systemBlue))
.cornerRadius(8.0)
}
}
ContentView to use the above view:
struct ContentView: View {
var body: some View {
VStack(alignment: .center, spacing: 10) {
Text("Disclosure Groups")
let groups: [String: [String]] = ["R1C1" : ["item 1", "item 2", "item 3", "item 4"],
"R1C2" : ["item 3", "item 4", "item 6", "item 7"],
"R2C1" : ["item 5", "item 6"],
"R2C2" : ["item 7", "item 8"]]
DisclosureGroupView(groups: groups)
.frame(height: 80)
Text("More Text here that is long")
.padding()
Text("Some More Text even wider than the first one")
}
}
}
Here is what I am getting:
StackOverFlow
The height is fixed because of the GeometryReader. I modified it by passing the width.
//
// SwiftUIView.swift
// asdadd
//
// Created by USER on 2021/08/04.
//
import SwiftUI
struct DisclosureGroupView: View {
var groups: [String: [String]]
var width: CGFloat
#State private var isExpanded: [String: Bool]
#State private var selectedItems = [String: Set<String>]()
#State private var isTitleChecked: [String: Bool]
init(groups: [String: [String]], width: CGFloat) {
self.groups = groups
self.width = width
_isExpanded = State(initialValue: groups.reduce(into: [String: Bool](), { result, group in
result[group.key] = false
}))
_isTitleChecked = State(initialValue: groups.reduce(into: [String: Bool](), {result, group in
result[group.key] = false
}))
}
var body: some View {
// GeometryReader { geo in
let ncols = 2
let width = width / CGFloat(ncols)
let layout = Array(repeating: GridItem(.fixed(width), spacing: 5), count: ncols)
LazyVGrid(columns: layout, alignment: .leading) {
ForEach(groups.keys.sorted(), id: \.self) { title in
self.disclosureGroup(title: title, items: groups[title, default: [String]()])
}
}
.border(Color.red)
// }
}
private func disclosureGroup(title: String, items: [String]) -> some View {
let expanded = Binding<Bool>(get: {isExpanded[title]!}, set: {isExpanded[title] = $0})
return DisclosureGroup(title, isExpanded: expanded) {
ScrollView {
VStack(alignment: .leading) {
ForEach(Array(items.enumerated()), id: \.1.hashValue) { index, item in
Text(item)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.zIndex(1.0)
}
}
.font(.body)
.frame(width: 110)
.accentColor(.white)
.foregroundColor(.white)
.padding(5)
.background(Color(UIColor.systemBlue))
.cornerRadius(8.0)
}
}
struct ContentView2: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center, spacing: 10) {
Text("Disclosure Groups")
let groups: [String: [String]] = ["R1C1" : ["item 1", "item 2", "item 3", "item 4"],
"R1C2" : ["item 3", "item 4", "item 6", "item 7"],
"R2C1" : ["item 5", "item 6"],
"R2C2" : ["item 7", "item 8"]]
DisclosureGroupView(groups: groups, width: geometry.size.width)
// .frame(height: 80)
Text("More Text here that is long")
.padding()
Text("Some More Text even wider than the first one")
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
I don't know if VStack's alignment doesn't work.
I moved it to the center using position.
GeometryReader { geometry in
VStack(spacing: 10) {
Text("Disclosure Groups")
let groups: [String: [String]] = ["R1C1" : ["item 1", "item 2", "item 3", "item 4"],
"R1C2" : ["item 3", "item 4", "item 6", "item 7"],
"R2C1" : ["item 5", "item 6"],
"R2C2" : ["item 7", "item 8"]]
DisclosureGroupView(groups: groups, width: geometry.size.width)
// .frame(height: 80)
Text("More Text here that is long")
.padding()
Text("Some More Text even wider than the first one")
}
.position(x: geometry.frame(in:.local).midX,
y: geometry.frame(in:.local).midY)
}

Content inside VStack shrinks when embedded inside a ScrollView in SwiftUI

I have a simple login screen with two textfield and a button. It should look like this. The two textfields closer together and the button a little ways down.
Here is my code.
struct ContentView: View {
var body: some View {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
}
}
struct InputTextField: View {
let title: String
#Binding var text: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.foregroundColor(.primary)
.fontWeight(.medium)
.font(.system(size: 18))
HStack {
TextField("", text: $text)
.frame(height: 54)
.textFieldStyle(PlainTextFieldStyle())
.cornerRadius(10)
}
.padding([.leading, .trailing], 10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 0.6))
}
.padding()
}
}
struct ActionButton: View {
let title: String
var action: () -> Void
var body: some View {
Button(title) {
action()
}
.frame(minWidth: 100, idealWidth: 100, maxWidth: .infinity, minHeight: 60, idealHeight: 60)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
.padding([.leading, .trailing])
.shadow(color: Color.gray, radius: 2, x: 0, y: 2)
}
}
I wanted to embed this inside a ScrollView so that user can scroll up and down when the keyboard comes up.
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
}
}
}
Here is where I'm coming across this issue. When I add the VStack inside a ScrollView, all the content kind of shrinks and shows clumped together. Seems like the Spacers have no effect when inside a ScrollView.
How can I fix this?
Demo project
Here, You need to make the content stretch to fill the whole scroll view by giving minimum height as below
struct ContentView: View {
var body: some View {
GeometryReader { gr in
ScrollView {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
.frame(minHeight: gr.size.height)
}
}
}
}
Here is output:
As you have found, Spacers behave differently when they are in a ScrollView or not, or put differently, when the axis they can expand on is infinite or finite.
If what you want is for your content to be centered vertically when it fits and scroll when it's larger than the screen, I would do something like this:
struct ContentView: View {
var body: some View {
VStack { // This new stack would vertically center the content
// (I haven't actually tried it though)
ScrollView {
VStack {
Spacer().size(height: MARGIN) // The minimum margin you want
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer().size(height: SPACING)
ActionButton(title: "Login", action: {})
Spacer().size(height: MARGIN)
}
}
}
}
}

Resources