I need to have a custom DisclosureGroup. I have a version of it working like this:
public struct CustomDisclosureGroup<LabelContent: View, Content: View>: View {
var label: LabelContent
var content: Content
#Binding var isExpanded: Bool
public init(isExpanded: Binding<Bool>, #ViewBuilder label: () -> LabelContent, #ViewBuilder content: () -> Content) {
self.label = label()
self.content = content()
self._isExpanded = isExpanded
}
public init(labelString: String, isExpanded: Binding<Bool>, #ViewBuilder content: () -> Content) {
self.init(isExpanded: isExpanded, label: { Text(labelString) as! LabelContent }, content: content)
}
public var body: some View {
DisclosureGroup(isExpanded: $isExpanded) {
content
} label: {
label
}
}
}
#main
struct TestDisclosureApp: App {
#StateObject var topModel = TopModel()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#EnvironmentObject var topModel: TopModel
#State var isExpanded1 = false
#State var isExpanded2 = false
var body: some View {
CustomDisclosureGroup(isExpanded: $isExpanded1, label: { Text("Label") }) {
HStack {
Text("Content1")
}
}
.padding()
// CustomDisclosureGroup<Text, View>(labelString: "Label", isExpanded: $isExpanded2) {
// HStack {
// Text("Content2")
// }
// }
// .padding()
}
}
I have two initializers. The first woks fine but the second one is giving me problems. Xcode somewhat forced me to add the type cast "as! LabelContent" which doesn't look nice. But, more importantly, I can't get it to work. Uncomment the second example to see the error message.
How can I declare an init to just take String instead of a LabelContent (i.e.: Label)?
If you use where to restrict the types you can define the type in the init
public struct CustomDisclosureGroup<LabelContent, Content>: View where LabelContent : View, Content : View{
Then in the init you can say that where LabelContent == Text
public init(isExpanded: Binding<Bool>, #ViewBuilder label: () -> LabelContent, #ViewBuilder content: () -> Content) {
self.label = label()
self.content = content()
self._isExpanded = isExpanded
}
public init(labelString: String, isExpanded: Binding<Bool>, #ViewBuilder content: () -> Content) where LabelContent == Text {
self.init(isExpanded: isExpanded, label: { Text(labelString)
}, content: content)
}
Full code below
public struct CustomDisclosureGroup<LabelContent, Content>: View where LabelContent : View, Content : View{
var label: LabelContent
var content: Content
#Binding var isExpanded: Bool
public init(isExpanded: Binding<Bool>, #ViewBuilder label: () -> LabelContent, #ViewBuilder content: () -> Content) {
self.label = label()
self.content = content()
self._isExpanded = isExpanded
}
public init(labelString: String, isExpanded: Binding<Bool>, #ViewBuilder content: () -> Content) where LabelContent == Text {
self.init(isExpanded: isExpanded, label: { Text(labelString)
}, content: content)
}
public var body: some View {
DisclosureGroup(isExpanded: $isExpanded) {
content
} label: {
label
}
}
}
Your second usage would look like this, there is no need to add types.
CustomDisclosureGroup(labelString: "Label", isExpanded: $isExpanded2) {
HStack {
Text("Content2")
}
}
.padding()
Related
I have an NSManagedObject with NSManaged properties that control the expanded/collapsed state of disclosure groups. Here's an example:
/// Views that provides UI for app settings.
struct SettingsView: View {
#Binding var isExpanded: Bool
var body: some View {
let _ = Self._printChanges()
DisclosureGroup(isExpanded: $isExpanded, content: {
VStack {
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
}
}, label: {
HStack {
Text("Settings")
}
})
.padding([.leading,.trailing])
}
}
The view's parent calls it like this:
#EnvironmentObject var settings: SideBarSettings
.
.
.
SettingsView(isExpanded: $settings.isExpanded)
The disclosure group animation is lost when using NSManaged property. Animation is preserved when using any other non-NSManaged property even if the property is declared inside NSManagedObject.
Why is DisclosureGroup animation lost when using the NSManaged property?
After spending a few days on this, I'm accepting that the underlying problem is the way NSManaged properties work with SwiftUI. So, a possible solution would be to not use the NSManaged property at all in the DisclosureGroup and use a value type instead.
And use modifiers to init it and track changes on the new State var; like this:
struct SettingsView: View {
#Binding var isExpanded: Bool
#State private var isExpandedNonManaged = false // New property
var body: some View {
let _ = Self._printChanges()
DisclosureGroup(isExpanded: $isExpandedNonManaged, content: {
VStack {
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
}
}, label: {
HStack {
Text("Settings")
}
})
.onAppear {
isExpandedNonManaged = isExpanded // Initialize the State property
}
.onChange(of: isExpandedNonManaged) { newValue in
isExpanded = newValue // Update the managed property
}
}
}
Not elegant, nor scalable ... but it works.
Open to better solutions!
Update:
With a little help from this post, came up with a CustomDisclosureGroup that eliminates lots of code duplication.
public struct CustomDisclosureGroup<LabelContent: View, Content: View>: View {
var label: LabelContent
var content: Content
#Binding var isExpanded: Bool
public init(isExpanded: Binding<Bool>, #ViewBuilder label: () -> LabelContent, #ViewBuilder content: () -> Content) {
self.label = label()
self.content = content()
self._isExpanded = isExpanded
}
public init(labelString: String, isExpanded: Binding<Bool>, #ViewBuilder content: () -> Content) where LabelContent == Text {
self.init(isExpanded: isExpanded, label: { Text(labelString) }, content: content)
}
public var body: some View {
DisclosureGroup(isExpanded: $isExpanded) {
content
} label: {
label
}
}
}
#main
struct TestDisclosureApp: App {
#StateObject var topModel = TopModel()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#EnvironmentObject var topModel: TopModel
#State var isExpanded1 = false
#State var isExpanded2 = false
var body: some View {
CustomDisclosureGroup(isExpanded: $isExpanded1, label: { Text("Label") }) {
HStack {
Text("Content1")
}
}
.padding()
CustomDisclosureGroup(labelString: "Label", isExpanded: $isExpanded2) {
HStack {
Text("Content2")
}
}
.padding()
}
}
I want to pass a Label to a custom UI component, but I can't figure out the syntax. Here's what I've got:
struct ProOnlyToggle: View {
var label: Label // ERROR: Reference to generic type 'Label' requires arguments in <...>
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
#EnvironmentObject var appSettings: AppSettingsModel
var body: some View {
if !appSettings.canUseProFeatures {
Toggle(isOn: $isOn, label: label)
.disabled(true)
.onTapGesture {
tappedCallback?()
}
} else {
Toggle(isOn: $isOn, label: label)
}
}
}
How does one pass a Label into a custom control?
More background:
I'm building a toggle for a feature which is only available to customers who pay. If the user hasn't paid the toggle should be disabled and tapping it should take some action. Here's the kind of thing in vanilla SwiftUI
Toggle(isOn: $isOn) {
Text("Some pro feature")
Text("more info")
.font(.footnote)
}
.disabled(!isPro ? true : false)
.onTapGesture {
if !isPro {
tappedCallback?()
}
}
Because I have quite a few of these switches, I'd like to make a reusable component that could be used like so:
// Note that is pro is passed into the #Environment
ProOnlyToggle(isOn: $settings.someToggle) {
Text("Turn a thing on")
Text("But be aware")
.font(.footnote)
} tappedCallback: {
// Do something in the UI
}
This feels quite simple, but I can't find any examples. What am I missing?
I came up with 3 solutions.
1. Use the generic View type instead of Label
struct ProOnlyToggle<T: View>: View {
init(#ViewBuilder label: #escaping ()->T, isOn: Binding<Bool>) {
self.label = label
self._isOn = isOn
}
let label: ()->T
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
var body: some View {
Toggle(isOn: $isOn, label: label)
.disabled(true)
.onTapGesture {
tappedCallback?()
}
}
}
Then, you can use the component like this:
struct TestView: View {
#State private var isON: Bool = false
var body: some View {
ProOnlyToggle(label: { Label(title: { Text("Title Text") },
icon: { Image(systemName: "heart") })},
isOn: $isON)
}
}
2. Use seperate Views for label's title and image
struct ProOnlyToggle<L: View, I: View>: View {
init(#ViewBuilder labelTitle: #escaping ()->L,
#ViewBuilder labelIcon: #escaping ()->I,
isOn: Binding<Bool>) {
self.labelTitle = labelTitle
self.labelIcon = labelIcon
self._isOn = isOn
}
let labelTitle: ()->L
let labelIcon: ()->I
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
var body: some View {
Toggle(isOn: $isOn, label: {
Label(title: labelTitle, icon: labelIcon)
})
.disabled(true)
.onTapGesture {
tappedCallback?()
}
}
}
Then you can use the component like this:
struct TestView: View {
#State private var isON: Bool = false
var body: some View {
ProOnlyToggle(labelTitle: {Text("LabelTitle")},
labelIcon: {Image(systemName: "heart")},
isOn: $isON)
}
}
3. Implement and use a custom Label view
struct CustomLabel: View {
let title: String
let systemImageName: String
var body: some View {
Label(title: { Text(title)}, icon: { Image(systemName: systemImageName) } )
}
}
Then, use the CustomLabel as a parameter in the "ProOnlyToggle" view.
struct ProOnlyToggle: View {
let label: CustomLabel
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
var body: some View {
Toggle(isOn: $isOn, label: {
label
})
.disabled(true)
.onTapGesture {
tappedCallback?()
}
}
}
And call the ProOnlyToggle anywhere in your app like this:
struct TestView: View {
#State private var isON: Bool = false
var body: some View {
ProOnlyToggle(label: CustomLabel(title: "Title Text",
systemImageName: "heart"),
isOn: $isON)
}
}
At this time, I have not found any better solutions. I hope it may help you.
I try to recreate the .toolbar modifier Apple uses for their NavigationView. I created an own implementation of a NavigationStackView but also want to use a .toolbar modifier.
I got something to work using environment objects and custom view modifiers, but when I don't apply the .toolbar modifier this won't work because no environment object is set.
Is there a better way to do this? How does Apple do this?
Example:
import Foundation
import SwiftUI
class ToolbarData: ObservableObject {
#Published var view: (() -> AnyView)? = nil
init(_ view: #escaping () -> AnyView) {
self.view = view
}
}
struct NavigationStackView<Content: View>: View {
#ViewBuilder var content: () -> Content
#EnvironmentObject var toolbar: ToolbarData
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
if (toolbar.view != nil) {
toolbar.view!()
}
}
Spacer()
content()
Spacer()
}
}
}
struct NavigationStackToolbar<ToolbarContent: View>: ViewModifier {
var toolbar: ToolbarContent
func body(content: Content) -> some View {
content
.environmentObject(ToolbarData({
AnyView(toolbar)
}))
}
}
extension NavigationStackView {
func toolbar<Content: View>(_ content: () -> Content) -> some View {
modifier(NavigationStackToolbar(toolbar: content()))
}
}
struct NavigationStackView_Previews: PreviewProvider {
static var previews: some View {
NavigationStackView {
Text("Test")
}
.toolbar {
Text("Toolbar")
}
}
}
Current solution:
import Foundation
import SwiftUI
private struct ToolbarEnvironmentKey: EnvironmentKey {
static let defaultValue: AnyView = AnyView(EmptyView())
}
extension EnvironmentValues {
var toolbar: AnyView {
get { self[ToolbarEnvironmentKey.self] }
set { self[ToolbarEnvironmentKey.self] = newValue }
}
}
struct NavigationStackView<Content: View>: View {
#ViewBuilder var content: () -> Content
#Environment(\.toolbar) var toolbar: AnyView
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
toolbar
}
Spacer()
content()
Spacer()
}
}
}
extension NavigationStackView {
func toolbar<Content: View>(_ content: () -> Content) -> some View {
self
.environment(\.toolbar, AnyView(content()))
}
}
struct NavigationStackView_Previews: PreviewProvider {
static var previews: some View {
NavigationStackView {
Text("Test")
}
.toolbar {
Text("Toolbar")
}
}
}
How do you drag to refresh a grid view in swiftui? I know you can do it with List view with refreshable modifier in iOS 15, but how can you do it with a LazyVGrid? How would you do it in either List or Grid view pre iOS 15? I pretty new at swiftui. I attached a gif showing what Im trying to achieve.
Drag to Refresh
Here is the code LazyVStack:
import SwiftUI
struct PullToRefreshSwiftUI: View {
#Binding private var needRefresh: Bool
private let coordinateSpaceName: String
private let onRefresh: () -> Void
init(needRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: #escaping () -> Void) {
self._needRefresh = needRefresh
self.coordinateSpaceName = coordinateSpaceName
self.onRefresh = onRefresh
}
var body: some View {
HStack(alignment: .center) {
if needRefresh {
VStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 100)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
value: $0.frame(in: .named(coordinateSpaceName)).origin.y)
})
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
guard !needRefresh else { return }
if abs(offset) > 50 {
needRefresh = true
onRefresh()
}
}
}
}
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
And here is typical usage:
struct ContentView: View {
#State private var refresh: Bool = false
#State private var itemList: [Int] = {
var array = [Int]()
(0..<40).forEach { value in
array.append(value)
}
return array
}()
var body: some View {
ScrollView {
PullToRefreshSwiftUI(needRefresh: $refresh,
coordinateSpaceName: "pullToRefresh") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation { refresh = false }
}
}
LazyVStack {
ForEach(itemList, id: \.self) { item in
HStack {
Spacer()
Text("\(item)")
Spacer()
}
}
}
}
.coordinateSpace(name: "pullToRefresh")
}
}
This can be easily adapted for LazyVGrid, just replace LazyVStack.
EDIT:
Here is more refined variant:
struct PullToRefresh: View {
private enum Constants {
static let refreshTriggerOffset = CGFloat(-140)
}
#Binding private var needsRefresh: Bool
private let coordinateSpaceName: String
private let onRefresh: () -> Void
init(needsRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: #escaping () -> Void) {
self._needsRefresh = needsRefresh
self.coordinateSpaceName = coordinateSpaceName
self.onRefresh = onRefresh
}
var body: some View {
HStack(alignment: .center) {
if needsRefresh {
VStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 60)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
value: -$0.frame(in: .named(coordinateSpaceName)).origin.y)
})
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
guard !needsRefresh, offset < Constants.refreshTriggerOffset else { return }
withAnimation { needsRefresh = true }
onRefresh()
}
}
}
private struct ScrollViewOffsetPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
private enum Constants {
static let coordinateSpaceName = "PullToRefreshScrollView"
}
struct PullToRefreshScrollView<Content: View>: View {
#Binding private var needsRefresh: Bool
private let onRefresh: () -> Void
private let content: () -> Content
init(needsRefresh: Binding<Bool>,
onRefresh: #escaping () -> Void,
#ViewBuilder content: #escaping () -> Content) {
self._needsRefresh = needsRefresh
self.onRefresh = onRefresh
self.content = content
}
var body: some View {
ScrollView {
PullToRefresh(needsRefresh: $needsRefresh,
coordinateSpaceName: Constants.coordinateSpaceName,
onRefresh: onRefresh)
content()
}
.coordinateSpace(name: Constants.coordinateSpaceName)
}
}
I am trying to make a list of items each item should open a sheet with SFSafariViewController in full screen. To show SFSafariViewController in full screen, I used the code available in this link: https://dev.to/uchcode/web-sheet-with-sfsafariviewcontroller-4nlc. The controller working very fine. However, when I put the items inside a List or ScrollView, it does not show the sheet. When I remove List/ScrollView it works fine.
Here is the code.
import SwiftUI
import SafariServices
struct RootView: View, Hostable {
#EnvironmentObject private var hostedObject: HostingObject<Self>
let address: String
let title: String
func present() {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = true
let safari = SFSafariViewController(url: URL(string: address)!, configuration: config)
hostedObject.viewController?.present(safari, animated: true)
}
var body: some View {
Button(title) {
self.present()
}
}
}
struct ContentView: View {
#State private var articlesList = [
ArticlesList(id: 0, title: "Apple", link: "http://apple.com", lang: "en"),
ArticlesList(id: 1, title: "Yahoo", link: "http://yahoo.com", lang: "en"),
ArticlesList(id: 2, title: "microsoft", link: "http://microsoft.com", lang: "en"),
ArticlesList(id: 3, title: "Google", link: "http://google.com", lang: "en")
]
var body: some View {
NavigationView{
//Here is the problem when I add RootView inside a List or ScrollView it does not show Safari.
ScrollView(.vertical, showsIndicators: false) {
VStack{
ForEach(articlesList) {article in
RootView(address: article.link, title: article.title).hosting()
Spacer(minLength: 10)
}
}
}
.navigationBarTitle("Articles")
}
}
}
struct ArticlesList: Identifiable, Codable {
let id: Int
let title: String
let link: String
let lang: String
}
struct UIViewControllerView: UIViewControllerRepresentable {
final class ViewController: UIViewController {
var didAppear: (UIViewController) -> Void = { _ in }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didAppear(self)
}
}
var didAppear: (UIViewController) -> Void
func makeUIViewController(context: Context) -> UIViewController {
let viewController = ViewController()
viewController.didAppear = didAppear
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
//
}
}
struct UIViewControllerViewModifier: ViewModifier {
var didAppear: (UIViewController) -> Void
var viewControllerView: some View {
UIViewControllerView(didAppear:didAppear).frame(width:0,height:0)
}
func body(content: Content) -> some View {
content.background(viewControllerView)
}
}
extension View {
func uiViewController(didAppear: #escaping (UIViewController) -> ()) -> some View {
modifier(UIViewControllerViewModifier(didAppear:didAppear))
}
}
class HostingObject<Content: View>: ObservableObject {
#Published var viewController: UIViewController? = nil
}
struct HostingObjectView<Content: View>: View {
var rootView: Content
let hostedObject = HostingObject<Content>()
func getHost(viewController: UIViewController) {
hostedObject.viewController = viewController.parent
}
var body: some View {
rootView
.uiViewController(didAppear: getHost(viewController:))
.environmentObject(hostedObject)
}
}
protocol Hostable: View {
associatedtype Content: View
func hosting() -> Content
}
extension Hostable {
func hosting() -> some View {
HostingObjectView(rootView: self)
}
}
Your code works in the iOS 14 simulator!
I wrapped SFSafariViewController, and if I replace your RootView with the following code, it works on my iOS 13 device. However it's not really full-screen but it's a sheet.
struct RootView: View {
#State private var isPresenting = false
let address: String
let title: String
var body: some View {
Button(self.title) {
self.isPresenting.toggle()
}
.sheet(isPresented: self.$isPresenting) {
SafariView(address: URL(string: self.address)!)
}
}
}
struct SafariView: UIViewControllerRepresentable {
let address: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = true
let safari = SFSafariViewController(url: self.address, configuration: config)
return safari
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
//
}
}
Below is the working code..
import SwiftUI
import SafariServices
struct ContentView: View {
var body: some View{
TabView {
HomeView()
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}.tag(0)
ArticlesView().hosting()
.tabItem{
VStack{
Image(systemName: "quote.bubble")
Text("Articles")
}
}.tag(1)
}
}
}
struct HomeView: View {
var body: some View{
Text("This is home")
}
}
struct ShareView: View{
var body: some View{
Text("Here the share")
}
}
struct ArticlesView: View, Hostable {
#EnvironmentObject private var hostedObject: HostingObject<Self>
#State private var showShare = false
#State private var articlesList = [
ArticlesList(id: 0, title: "Apple", link: "http://apple.com", lang: "en"),
ArticlesList(id: 1, title: "Yahoo", link: "http://yahoo.com", lang: "en"),
ArticlesList(id: 2, title: "microsoft", link: "http://microsoft.com", lang: "en"),
ArticlesList(id: 3, title: "Google", link: "http://google.com", lang: "en")
]
func present(address: String) {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = true
let safari = SFSafariViewController(url: URL(string: address)!, configuration: config)
hostedObject.viewController?.present(safari, animated: true)
}
var body: some View {
NavigationView{
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 40){
ForEach(articlesList) {article in
Button(article.title) {
self.present(address: article.link)
}
}
}
.sheet(isPresented: $showShare){
ShareView()
}
.navigationBarTitle("Articles")
.navigationBarItems(leading:
Button(action: {
self.showShare.toggle()
})
{
Image(systemName: "plus")
}
)
}
}
}
}
struct ArticlesList: Identifiable, Codable {
let id: Int
let title: String
let link: String
let lang: String
}
struct UIViewControllerView: UIViewControllerRepresentable {
final class ViewController: UIViewController {
var didAppear: (UIViewController) -> Void = { _ in }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didAppear(self)
}
}
var didAppear: (UIViewController) -> Void
func makeUIViewController(context: Context) -> UIViewController {
let viewController = ViewController()
viewController.didAppear = didAppear
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
//
}
}
struct UIViewControllerViewModifier: ViewModifier {
var didAppear: (UIViewController) -> Void
var viewControllerView: some View {
UIViewControllerView(didAppear:didAppear).frame(width:0,height:0)
}
func body(content: Content) -> some View {
content.background(viewControllerView)
}
}
extension View {
func uiViewController(didAppear: #escaping (UIViewController) -> ()) -> some View {
modifier(UIViewControllerViewModifier(didAppear:didAppear))
}
}
class HostingObject<Content: View>: ObservableObject {
#Published var viewController: UIViewController? = nil
}
struct HostingObjectView<Content: View>: View {
var rootView: Content
let hostedObject = HostingObject<Content>()
func getHost(viewController: UIViewController) {
hostedObject.viewController = viewController.parent
}
var body: some View {
rootView
.uiViewController(didAppear: getHost(viewController:))
.environmentObject(hostedObject)
}
}
protocol Hostable: View {
associatedtype Content: View
func hosting() -> Content
}
extension Hostable {
func hosting() -> some View {
HostingObjectView(rootView: self)
}
}