Recursive Disclosure Groups in SwiftUI - recursion

I would like to show a directory structure with all nodes expanded in SwiftUI.
I found an example of a recursive disclosure view structure on the internet.
The view:
struct TracksDirOutlineView<Node>: View where Node: Hashable, Node: Identifiable, Node: CustomStringConvertible{
let node: Node
let childKeyPath: KeyPath<Node, [Node]?>
#State var isExpanded: Bool = true
var body: some View {
GeometryReader { geometry in
if node[keyPath: childKeyPath] != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node[keyPath: childKeyPath]!) { childNode in
TracksDirOutlineView(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded)
}
}
},
label: { Text(node.description) })
} else {
Text(node.description)
}
}
}
}
The model / view model:
struct FileItem: Hashable, Identifiable, CustomStringConvertible {
var id: Self { self }
var name: String
var children: [FileItem]? = nil
var description: String {
switch children {
case nil:
return "📄 \(name)"
case .some(let children):
return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
}
}
}
let data =
FileItem(name: "root", children:
[FileItem(name: "child-1", children:
[FileItem(name: "child-1-1", children:
[FileItem(name: "child-1-1-1"),
FileItem(name: "child-1-1-2")]),
FileItem(name: "child-1-2", children:
[FileItem(name: "child-1-2-1")]),
FileItem(name: "child-1-3", children: [])
]),
FileItem(name: "child-2", children:
[FileItem(name: "child-2-1", children: [])
])
])
The call in ContentView:
TracksDirOutlineView(node: data, childKeyPath: \.children)
The result:
Somehow the view is cut off, even if there is still plenty of space in the view.
I am not sure, if and where there should be a frame modifier.
If I remove the GeometryReader, then the view becomes very large and everything is pushed to the bottom and partly off-screen
Also, is this the right approach? Should I use NSRepesentable with the NSOutlineView?

It looks like this could be fixed by just replacing the Geometry Reader with VStack and adding a Spacer() at the bottom, e.g:
VStack {
if node[keyPath: childKeyPath] != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node[keyPath: childKeyPath]!) { childNode in
TracksDirOutlineView(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded)
}
}
},
label: { Text(node.description) })
} else {
Text(node.description)
}
Spacer()
}

Related

Is it possible to create a LazyVGrid without the Lazy modifier?

I'm loading data in from my Firebase backend, the "lazy" part makes my app look glitchy/frozen-like when scrolling down, it lags heavily...
Is it possible to create a VGrid "without the lazy functionality"??
(iOS 14)
If not, any suggestions other than ditching the Grid look altogether?
let layout = [
GridItem(.flexible()),
GridItem(.flexible()),
]
#ObservedObject var homeModel = Home_ViewModel()
NavigationView(content: {
ScrollView() {
LazyVGrid(columns: layout, spacing: 10) {
ForEach(homeModel.projectList) { item in
ProjectItemWidget(
projectID: item.id,
projectTitle: item.projectTitle,
projectAuthorProfileImage: item.authorProfileImageUrl,
projectAuthor: item.projectAuthor)
}
}
.padding(.trailing, 7.5)
}
}
I was struggling thinking in a solution where a could create a grid layout without using LazyVGrid and came up with the following:
extension Array {
func getElementAt(index: Int) -> Element? {
return (index < self.endIndex) ? self[index] : nil
}
}
struct CustomGridLayout<Element, GridCell>: View where GridCell: View {
private var array: [Element]
private var numberOfColumns: Int
private var gridCell: (_ element: Element) -> GridCell
init(_ array: [Element], numberOfColumns: Int, #ViewBuilder gridCell: #escaping (_ element: Element) -> GridCell) {
self.array = array
self.numberOfColumns = numberOfColumns
self.gridCell = gridCell
}
var body: some View {
Grid {
ForEach(Array(stride(from: 0, to: self.array.count, by: self.numberOfColumns)), id: \.self) { index in
GridRow {
ForEach(0..<self.numberOfColumns, id: \.self) { j in
if let element = self.array.getElementAt(index: index + j) {
self.gridCell(element)
}
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
An example using in a View:
struct ContentView: View {
private var array: [Int] = Array(1...7)
var body: some View {
CustomGridLayout(array, numberOfColumns: 3) { element in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.orange)
.overlay(alignment: .center) {
Text("\(element)")
}
}
.padding(.horizontal)
}
}
You can see the result in the following link: https://i.stack.imgur.com/1o7ip.png

Result of 'HymnLyrics' initializer is unused

I have the following error in my code. Please help me why I cannot use my HymnLyrics() struct inside button action. It works for NavigationLink of destination but I don't wanna use it.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var tappingSwitches: TapToggle
let zoLyrics: [Lyric] = LyricList.hymnLa.sorted { lhs, rhs in
return lhs.zoTitle < rhs.zoTitle
}
var body: some View {
ScrollView {
ForEach(zoLyrics, id: \.id) { zoLyric in
VStack {
Button(action: {
HymnLyrics(lyrics: LyricList.hymnLa)// Here is the error, I can use directly by using NavagationLink
self.tappingSwitches.isHymnTapped.toggle()
}, label: {
HStack {
Text(zoLyric.zoTitle)
.foregroundColor(Color("bTextColor"))
.lineLimit(1)
.minimumScaleFactor(0.5)
Spacer()
Text("\(zoLyric.number)")
.foregroundColor(Color("bTextColor"))
}
})
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.leading, .bottom, .trailing])
}
}
}
}

SwiftUI using Environment Object on multiple views giving issues with Navigation

Don't know if I'm abusing the idea of environment object, but experiencing an issue when using an environment object that publishes a delayed async value. One view navigates to the next, but then the 'root' gets updated subsequently and as a result causes an 'echo', or even if that is handled a navigation problem. The issue becomes even more evident when using transitions between navigation.
Is there a correct use pattern to avoid this? Or some other solution maybe?
Any guidance will be appreciated.
Attached a condensed sample to illustrate the problem.
Xcode 12.4 ios 14.1
final class SetColor: ObservableObject {
#Published var asyncVal: Bool = false
func flipIt() {
DispatchQueue.main.asyncAfter(deadline: .now()+0.5, execute: {self.asyncVal.toggle()})
}
}
struct HomeView: View {
#StateObject var setCol: SetColor = SetColor()
#State private var navActive: Bool = false
var body: some View {
NavigationView {
ZStack {
Color(setCol.asyncVal ? .blue : .purple)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Home")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild1().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 1 >") })
}
}
}
}
}
}
struct NavChild1: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 1")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild2().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 2 >") })
}
}
}
}
}
struct NavChild2: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 2")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild3().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 3 >") })
}
}
}
}
}
struct NavChild3: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 3")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild3().environmentObject(setCol), isActive: .constant(false), label: { Text("Go Home") })
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
You do not need the deadline you put in GCD action. It causes navigation actions even if user does not press on navigation (I've tested the code in a project).
This is because you accumulate jobs in the GCD queue and when they are executed, you're in another View (due to the 0.5 stall). By the way, they cause navigation since the flip is Observed and therefore whoever listens , will execute the navigation.
Anyway, what you wanna do is change the dispatch command to this:
DispatchQueue.main.async { self.asyncVal.toggle() }
And navigation will be smoother with no extra navigation commands executed afterwards.

Custom Button with Multiple NavigationLink

I want to be able to use my custom button in multiple places. Right now it can only Navigate to StartWithPhoneView when tapped. When I use somewhere else I want to Navigate to another view too. I can do it by creating two custom buttons but it is code repetition.
struct CustomButtonView: View {
#State var isTapped: Bool = false
var text = ""
var body: some View {
Button(action: {
print("Create account tapped")
self.isTapped.toggle()
}, label: {
RoundedRectangle(cornerRadius: 5)
.frame(width: UIScreen.main.bounds.width / 1.205, height: 44)
.foregroundColor(.blue)
.overlay(Text(text))
.foregroundColor(.white)
})
.padding(.top,15)
NavigationLink("", destination: StartWithPhoneView(), isActive: $isTapped)
}
}
I am using Custom Button in this SignUpView
struct SignupView: View {
var body: some View {
NavigationView {
VStack {
CustomButtonView(text: "Create an account" )
}
NavigationLink("", destination: StartWithPhoneView(), isActive: CustomButtonView.$isTapped) // I want to reach inside CustomButtonView to fetch isTapped
}
}
}
You can use Generic type which is a View
struct CustomButtonView<Destination: View>: View { //<-here
#State var isTapped: Bool = false
var destination: Destination //<- here
var text = ""
var body: some View {
Button(action: {
print("Create account tapped")
self.isTapped.toggle()
}, label: {
RoundedRectangle(cornerRadius: 5)
.frame(width: UIScreen.main.bounds.width / 1.205, height: 44)
.foregroundColor(.blue)
.overlay(Text(text))
.foregroundColor(.white)
})
.padding(.top,15)
NavigationLink("bbbbb", destination: destination, isActive: $isTapped) //<- here
}
}

Init Custom Button SwiftUI

Trying to init CustomButton(title: "Add", icon: .add, status: .enable)
My code is below. I do get the title but enums are not working.
Plus recieving error
Cannot convert value of type 'Image' to expected argument type 'String'
at Image(icon)
import SwiftUI
struct CustomButton: View {
var title: String
var icon: String
var status: Color
var body: some View {
Button(action: {
}) {
Text(title)
.foregroundColor(.white)
.background(Color(.green))
.font(Font.custom("SFCompactDisplay", size: 14))
Image(icon)
.renderingMode(.original)
.foregroundColor(.white)
}
}
enum Icon {
case add
case edit
var image: Image {
switch self {
case .add:
return Image("Add")
case .edit:
return Image("Edit")
}
}
}
enum Status {
case enable
case disable
var color : Color {
switch self {
case .enable:
return Color(.green)
case .disable:
return Color(.gray)
}
}
}
init(title: String, icon: Icon, status: Status) {
self.title = title
self.icon = icon.image
self.status = status.color
}
}
I assume you wanted this
struct CustomButton: View {
var title: String
var icon: Icon
var status: Color
var body: some View {
Button(action: {
}) {
Text(title)
.foregroundColor(.white)
.background(Color(.green))
.font(Font.custom("SFCompactDisplay", size: 14))
icon.image
.renderingMode(.original)
.foregroundColor(.white)
}
}
enum Icon {
case add
case edit
var image: Image {
switch self {
case .add:
return Image("Add")
case .edit:
return Image("Edit")
}
}
}
enum Status {
case enable
case disable
var color : Color {
switch self {
case .enable:
return Color(.green)
case .disable:
return Color(.gray)
}
}
}
init(title: String, icon: Icon, status: Status) {
self.title = title
self.icon = icon
self.status = status.color
}
}
I figured it out. It works now.
struct CustomButton: View {
let title: String
let icon : String
let status: Color
#State private var buttonDisabled = true
var body: some View {
Button(action: {
}) {
ZStack(alignment:.bottom) {
HStack {
Text(title)
.foregroundColor(.white)
.font(Font.custom("SFCompactDisplay-Bold", size: 20))
.bold()
.fontWeight(.bold)
.background(status)
Image(icon)
.renderingMode(.original)
.foregroundColor(.white)
.background(Color(.white))
}
.frame(width: 335, height: 20, alignment: .center)
.padding()
.background(status)
}
.cornerRadius(10)
}
}
enum Icon {
case add
case edit
case none
var image: String {
switch self {
case .add:
return "Add"
case .edit:
return "Edit"
case .none:
return "empty"
}
}
}
enum Status {
case enable
case disable
}
init(title: String, icon: Icon, status: Status) {
self.title = title
self.icon = icon.image
if status == .enable {
self.status = Color(#colorLiteral(red: 0, green: 0.6588235294, blue: 0.5254901961, alpha: 1))
} else {
self.status = Color(#colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1))
}
}
}
struct CustomButton_Previews: PreviewProvider {
static var previews: some View {
CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable)
}
}

Resources