I want to be able to click a GroupBox and it navigate me to another view. I tried implementing the following code and got the errors :
Cannot convert value of type 'NavigationLink<Text, some View>' to expected argument type 'GroupBoxStyleConfiguration'
Type '() -> ()' cannot conform to 'StringProtocol'; only struct/enum/class types can conform to protocols
I wanted to know if there is a way to use Groupboxes to change views and if so how ?
import SwiftUI
struct ContentView: View {
// MARK - Properties
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10) {
// MARK: - SECTION 1 : Daily Catch-up
GroupBox(
// The errors start below
NavigationLink({
WellnessLabelView(labelText: "Daily Catch-up", labelImage: "chevron.right")
) {
Divider().padding(.vertical, 4)
HStack(alignment: .center, spacing: 10) {
Text("This app was developed with the idea that expectant families deserve easy access to resources.")
.font(.footnote)}
}
You have to make your own GroupBoxStyle https://itnext.io/swiftui-groupbox-for-ios-bb16aa71469c
struct HealthGroupBoxStyle<V: View>: GroupBoxStyle {
var color: Color
var destination: V
var date: Date?
#ScaledMetric var size: CGFloat = 1
func makeBody(configuration: Configuration) -> some View {
NavigationLink(destination: destination) {
GroupBox(label: HStack {
configuration.label.foregroundColor(color)
Spacer()
if date != nil {
Text("\(date!)").font(.footnote).foregroundColor(.secondary).padding(.trailing, 4)
}
Image(systemName: "chevron.right").foregroundColor(Color(.systemGray4)).imageScale(.small)
}) {
configuration.content.padding(.top)
}
}.buttonStyle(PlainButtonStyle())
}
}
Related
Foreach on view must be presented with a View to process.
struct Home : View {
private var numberOfImages = 3
#State var isPresented : Bool = false
#State var currentImage : String = ""
var body: some View {
VStack {
TabView {
ForEach(1..<numberOfImages+1, id: \.self) { num in
Image("someimage")
.resizable()
.scaledToFill()
.onTapGesture() {
currentImage = "top_00\(num)"
isPresented.toggle()
}
}
}.fullScreenCover(isPresented: $isPresented, content: {FullScreenModalView(imageName: currentImage) } )
}
}
I'm trying to display an image in fullScreenCover. My problem is that the first image is empty. Yes, we can solve this defining at the beginning, however, this will complicate the code according to my experiences.
My question is, is it possible to assign a value to currentImage before the onTapGesture processed.
In short, what is the good practice here.
What you need is to use this modifier to present your full screen modal:
func fullScreenCover<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: #escaping (Item) -> Content) -> some View where Item : Identifiable, Content : View
You pass in a binding to an optional and uses the non optional value to construct a destination:
struct ContentView: View {
private let imageNames = ["globe.americas.fill", "globe.europe.africa.fill", "globe.asia.australia.fill"]
#State var selectedImage: String?
var body: some View {
VStack {
TabView {
ForEach(imageNames, id: \.self) { imageName in
Image(systemName: imageName)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageName
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageName in
Destination(imageName: imageName)
}
}
}
}
struct Destination: View {
let imageName: String
var body: some View {
ZStack {
Color.blue
Image(
systemName: imageName
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
}
}
You will have to make String identifiable for this example to work (not recommended):
extension String: Identifiable {
public var id: String { self }
}
Building upon #LuLuGaGa’s answer (accept that answer, not this one), instead of making String Identifiable, create a new Identifiable struct to hold the image info. This guarantees each image will have a unique identity, even if they use the same base image name. Also, the ForEach loop now becomes ForEach(imageInfos) since the array contains Identifiable objects.
Use map to turn image name strings into [ImageInfo] by calling the ImageInfo initializer with each name.
This example also puts the displayed image into its own view which can be dismissed with a tap.
import SwiftUI
struct ImageInfo: Identifiable {
let name: String
let id = UUID()
}
struct ContentView: View {
private let imageInfos = [
"globe.americas.fill",
"globe.europe.africa.fill",
"globe.asia.australia.fill"
].map(ImageInfo.init)
#State var selectedImage: ImageInfo?
var body: some View {
VStack {
TabView {
ForEach(imageInfos) { imageInfo in
Image(systemName: imageInfo.name)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageInfo
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageInfo in
ImageDisplay(info: imageInfo)
}
}
}
}
struct ImageDisplay: View {
let info: ImageInfo
#Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
Color.blue
Image(
systemName: info.name
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
dismiss()
}
}
}
UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}
I have a problem with SwiftUI
There was a issue
yello view don't appear at first launch view
move to home that makes app into background mode
move to app (foreground)
then yellow view appears
I captured view hierarchy in step 1.
(left is view hierarchy capture, right is simulator)
view hierarchy capture shows the yellow square but simulator didn't show yellow square
I checked the view breadscrum but both was same so I have no clue.
I'm sure this is not a networking problem.
There is two way to appear yellow square
background -> foreground
present alert -> dismiss alert
I'm not sure this is a framework bug or else.
Also, is there any API that I can print the swiftUI rendering request succeed or fail?
Thank you in advance and merry christmas!
(left is before yello square appear on simulator/ right is after yello square appear on simulator)
edit - add sample code
contentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel : viewModel
#ObservedObject var params : otherViewModel
var body: some View {
HorizontalScrollView(viewModel: viewModel, someParmas: params)
.padding(.leading, 24)
.frame(width: UIScreen.main.bounds.width, height:400)
.background(Color.red)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: viewModel(homeAPI: HomeAPI()), params: otherViewModel())
}
}
import SwiftUI
struct HorizontalScrollView: View {
#ObservedObject var viewModel: viewModel
#ObservedObject var holder : otherViewModel
private var homeHightlightRange: Range<Int> {
return 0..<(viewModel.something?.somethingList?.count ?? 0)
}
init(viewModel: viewModel, someParmas: otherViewModel) {
self.viewModel = viewModel
self.holder = someParmas
}
var body: some View {
VStack(spacing: 20) {
HStack(spacing: 0) {
Text("Merry christmas")
.font(.system(size: 24))
.foregroundColor(.white)
.bold()
.onTapGesture {
viewModel.getHighlight()
}
Spacer()
}
ScrollView(.horizontal, showsIndicators: false, content: {
HStack(alignment:.bottom, spacing: 14) {
ForEach(homeHightlightRange, id: \.self) { index in
Color.yellow.frame(width:200, height:300)
}
}
})
}
.frame(height: 339)
}
}
struct HighlightView_Previews: PreviewProvider {
static var previews: some View {
HorizontalScrollView(viewModel: viewModel(homeAPI: HomeAPI()),someParmas: otherViewModel())
.previewLayout(.sizeThatFits)
}
}
homeHightlightRange is got from server via viewModel
I suppose that the solution is to add ScrollView conditionally on API results appear, like (not tested - typed here, so typos might be present)
if viewModel.something?.somethingList?.count != 0 {
ScrollView(.horizontal, showsIndicators: false, content: {
HStack(alignment:.bottom, spacing: 14) {
ForEach(homeHightlightRange, id: \.self) { index in
Color.yellow.frame(width:200, height:300)
}
}
})
}
There are a lot of solutions for trying to align multiple images and text in SwiftUI using a HStacks inside of a VStack. Is there any way to do it for multiple Labels? When added in a list, multiple labels automatically align vertically neatly. Is there a simple way to do this for when they are embedded inside of a VStack?
struct ContentView: View {
var body: some View {
// List{
VStack(alignment: .leading){
Label("People", systemImage: "person.3")
Label("Star", systemImage: "star")
Label("This is a plane", systemImage: "airplane")
}
}
}
So, you want this:
We're going to implement a container view called EqualIconWidthDomain so that we can draw the image shown above with this code:
struct ContentView: View {
var body: some View {
EqualIconWidthDomain {
VStack(alignment: .leading) {
Label("People", systemImage: "person.3")
Label("Star", systemImage: "star")
Label("This is a plane", systemImage: "airplane")
}
}
}
}
You can find all the code in this gist.
To solve this problem, we need to measure each icon's width, and apply a frame to each icon, using the maximum of the widths.
SwiftUI provides a system called “preferences” by which a view can pass a value up to its ancestors, and the ancestors can aggregate those values. To use it, we create a type conforming to PreferenceKey, like this:
fileprivate struct IconWidthKey: PreferenceKey {
static var defaultValue: CGFloat? { nil }
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
switch (value, nextValue()) {
case (nil, let next): value = next
case (_, nil): break
case (.some(let current), .some(let next)): value = max(current, next)
}
}
}
To pass the maximum width back down to the labels, we'll use the “environment” system. For that, we need an EnvironmentKey. In this case, we can use IconWidthKey again. We also need to add a computed property to EnvironmentValues that uses the key type:
extension IconWidthKey: EnvironmentKey { }
extension EnvironmentValues {
fileprivate var iconWidth: CGFloat? {
get { self[IconWidthKey.self] }
set { self[IconWidthKey.self] = newValue }
}
}
Now we need a way to measure an icon's width, store it in the preference, and apply the environment's width to the icon. We'll create a ViewModifier to do those steps:
fileprivate struct IconWidthModifier: ViewModifier {
#Environment(\.iconWidth) var width
func body(content: Content) -> some View {
content
.background(GeometryReader { proxy in
Color.clear
.preference(key: IconWidthKey.self, value: proxy.size.width)
})
.frame(width: width)
}
}
To apply the modifier to the icon of each label, we need a LabelStyle:
struct EqualIconWidthLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon.modifier(IconWidthModifier())
configuration.title
}
}
}
Finally, we can write the EqualIconWidthDomain container. It needs to receive the preference value from SwiftUI and put it into the environment of its descendants. It also needs to apply the EqualIconWidthLabelStyle to its descendants.
struct EqualIconWidthDomain<Content: View>: View {
let content: Content
#State var iconWidth: CGFloat? = nil
init(#ViewBuilder _ content: () -> Content) {
self.content = content()
}
var body: some View {
content
.environment(\.iconWidth, iconWidth)
.onPreferenceChange(IconWidthKey.self) { self.iconWidth = $0 }
.labelStyle(EqualIconWidthLabelStyle())
}
}
Note that EqualIconWidthDomain doesn't just have to be a VStack of Labels, and the icons don't have to be SF Symbols images. For example, we can show this:
Notice that one of the label “icons” is an emoji in a Text. All four icons are laid out with the same width (across both columns). Here's the code:
struct FancyView: View {
var body: some View {
EqualIconWidthDomain {
VStack {
Text("Le Menu")
.font(.caption)
Divider()
HStack {
VStack(alignment: .leading) {
Label(
title: { Text("Strawberry") },
icon: { Text("🍓") })
Label("Money", systemImage: "banknote")
}
VStack(alignment: .leading) {
Label("People", systemImage: "person.3")
Label("Star", systemImage: "star")
}
}
}
}
}
}
This has been driving me crazy myself for a while. One of those things where I kept approaching it the same incorrect way - by seeing it as some sort of alignment configuration that was inside the black box that is List.
However it appears that it is much simpler. Within the List, Apple is simply applying a ListStyle - seemingly one that is not public.
I created something that does a pretty decent job like this:
public struct ListLabelStyle: LabelStyle {
#ScaledMetric var padding: CGFloat = 6
public func makeBody(configuration: Configuration) -> some View {
HStack {
Image(systemName: "rectangle")
.hidden()
.padding(padding)
.overlay(
configuration.icon
.foregroundColor(.accentColor)
)
configuration.title
}
}
}
This uses a hidden rectangle SFSymbol to set the base size of the icon. This is not the widest possible icon, however visually it seems to work well. In the sample below, you can see that Apple's own ListStyle assumes that the label icon will not be something significantly larger than the SFSymbol with the font being used.
While the sample here is not pixel perfect with Apple's own List, it's close and with some tweaking, you should be able to achieve what you are after.
By the way, this works with dynamic type as well.
Here is the complete code I used to generate this sample.
public struct ListLabelStyle: LabelStyle {
#ScaledMetric var padding: CGFloat = 6
public func makeBody(configuration: Configuration) -> some View {
HStack {
Image(systemName: "rectangle")
.hidden()
.padding(padding)
.overlay(
configuration.icon
.foregroundColor(.accentColor)
)
configuration.title
}
}
}
struct ContentView: View {
#ScaledMetric var rowHeightPadding: CGFloat = 6
var body: some View {
VStack {
Text("Lazy VStack Plain").font(.title2)
LazyVStack(alignment: .leading) {
ListItem.all
}
Text("Lazy VStack with LabelStyle").font(.title2)
LazyVStack(alignment: .leading, spacing: 0) {
vStackContent
}
.labelStyle(ListLabelStyle())
Text("Built in List").font(.title2)
List {
ListItem.all
labelWithHugeIcon
labelWithCircle
}
.listStyle(PlainListStyle())
}
}
// MARK: List Content
#ViewBuilder
var vStackContent: some View {
ForEach(ListItem.allCases, id: \.rawValue) { item in
vStackRow {
item.label
}
}
vStackRow { labelWithHugeIcon }
vStackRow { labelWithCircle }
}
func vStackRow<Content>(#ViewBuilder _ content: () -> Content) -> some View where Content : View {
VStack(alignment: .leading, spacing: 0) {
content()
.padding(.vertical, rowHeightPadding)
Divider()
}
.padding(.leading)
}
// MARK: List Content
var labelWithHugeIcon: some View {
Label {
Text("This is HUGE")
} icon: {
HStack {
Image(systemName: "person.3")
Image(systemName: "arrow.forward")
}
}
}
var labelWithCircle: some View {
Label {
Text("Circle")
} icon: {
Circle()
}
}
enum ListItem: String, CaseIterable {
case airplane
case people = "person.3"
case rectangle
case chevron = "chevron.compact.right"
var label: some View {
Label(self.rawValue, systemImage: self.rawValue)
}
static var all: some View {
ForEach(Self.allCases, id: \.rawValue) { item in
item.label
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
// .environment(\.sizeCategory, .extraExtraLarge)
}
}
Combining a few of these answers into another simple option (Very similar to some of the other options but thought it was distinct enough that some may find it useful). This has the simplicity of just setting a frame on the icon, and the swiftUI-ness of using LabelStyle but still adapts to dynamic type!
struct StandardizedIconWidthLabelStyle: LabelStyle {
#ScaledMetric private var size: CGFloat = 25
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.frame(width: size, height: size)
}
}
}
The problem is that the system icons have different standard widths. It's probably easiest to use an HStack as you mentioned. However, if you use the full Label completion, you'll see that the Title is actually just a Text and the icon is just an Image... and you can then add custom modifiers, such as a specific frame for the image width. Personally, I'd rather just use an HStack anyway.
var body: some View {
VStack(alignment: .leading){
Label(
title: {
Text("People")
},
icon: {
Image(systemName: "person.3")
.frame(width: 30)
})
Label(
title: {
Text("Star")
},
icon: {
Image(systemName: "star")
.frame(width: 30)
})
Label(
title: {
Text("This is a plane")
},
icon: {
Image(systemName: "airplane")
.frame(width: 30)
})
}
}
Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}