Some usages of custom generic view modifiers in SwiftUI do not compile - swiftui

I am trying to replace a standard sheet modifier with a custom one that applies the same changes to the content of all sheets as to the main view (it can be useful for changing accent color, although there is a UIKit approach for it, but specifically I want to apply privacySensitive modifier to all sheets).
The code that creates the modifiers compiles ok:
import SwiftUI
struct SheetForItem<T, C>: ViewModifier where T: Identifiable, C: View {
var item: Binding<T?>
var onDismiss: (() -> Void)? = nil
var sheetContent: (T) -> C
func body(content: Content) -> some View {
content.sheet(item: item, onDismiss: onDismiss) {
sheetContent($0).privacySensitive()
}
}
}
extension View {
func appSheet<T, Content>(
item: Binding<T?>,
onDismiss: (() -> Void)? = nil,
content: #escaping (T) -> Content
) -> some View where T: Identifiable, Content: View {
modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content))
}
}
Mostly it works, but some of the usages of appSheet in the chain of other modifiers instead of sheet do not compile with an error:
Type () cannot conform to View.
The example below doesn't compile (but it will compile if I replace appSheet with sheet):
import SwiftUI
enum Actions:Identifiable {
case action1
case action2
var id: Self { self }
}
struct AppSheetExample: View {
#State var showActions = false
#State private var action: Actions?
var body: some View {
Button { showActions = true } label: {
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.confirmationDialog("Actions", isPresented: $showActions, titleVisibility: .visible) {
Button("Action 1") { action = .action2 }
Button("Action 2") { action = .action2 }
}
.appSheet(item: $action) { sheet in
switch sheet {
case .action1: Text("Action 1")
case .action2: Text("Action 2")
}
}
}
}
Thank you!

You need to mark your content closure with #ViewBuilder since you're not explicitly returning a View(i.e: return Text("Action 1")):
extension View {
func appSheet<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
#ViewBuilder content: #escaping () -> Content
) -> some View where Content: View {
modifier(SheetIsPresented(isPresented: isPresented, onDismiss: onDismiss, sheetContent: content))
}
func appSheet<T, Content>(
item: Binding<T?>,
onDismiss: (() -> Void)? = nil,
#ViewBuilder content: #escaping (T) -> Content
) -> some View where T: Identifiable, Content: View {
modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content))
}
}

Related

SwiftUI can't select ONE item when add ViewModifier

After I added ViewModifier, I can't use the mouse to select one item (contrl) on "Selectable Mode" (Not Live Mode) at Previews window (Canvas), only Multiple Selection can be selected, please how can I change the code of "ContentView_Preview" to correct it ?
[Add picture][1] [1]: https://i.stack.imgur.com/y6oF0.jpg
import SwiftUI
struct ContentView: View {
#State private var vShift: Bool = false
var body: some View {
VStack {
Text ("Pressed Shift: " + String(vShift))
Button("Shift-Button") {
vShift = false
}
.padding()
.background(Color.yellow)
.pressAction {
vShift = true
} onRelease: {
vShift = false
}
}
}
}
struct PressActions: ViewModifier {
var onPress: () -> Void
var onRelease: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
onPress()
})
.onEnded({ _ in
onRelease()
})
)
}
}
extension View {
func pressAction(onPress: #escaping (() -> Void), onRelease: #escaping (() -> Void)) -> some View {
modifier(PressActions(onPress: {
onPress()
}, onRelease: {
onRelease()
}))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to make custom .sheet viewmodifier

I am trying to recreate the native .sheet() view modifier in SwiftUI. When I look at the definition, I get below function, but I'm not sure where to go from there.
The .sheet somehow passes a view WITH bindings to a distant parent at the top of the view-tree, but I can't see how that is done. If you use PreferenceKey with an AnyView, you can't have bindings.
My usecase is that I want to define a sheet in a subview, but I want to activate it at a distant parent-view to avoid it interfering with other code.
func showSheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, #ViewBuilder content: #escaping () -> Content) -> some View where Content : View {
// What do I put here?
}
So, I ended up doing my own sheet in SwiftUI using a preferenceKey for passing the view up the view-tree, and an environmentObject for passing the binding for showing/hiding the sheet back down again.
It's a bit long-winded, but here's the gist of it:
struct HomeOverlays<Content: View>: View {
#Binding var showSheet:Bool
#State private var sheet:EquatableViewContainer = EquatableViewContainer(id: "original", view: AnyView(Text("No view")))
#State private var animatedSheet:Bool = false
#State private var dragPercentage:Double = 0 /// 1 = fully visible, 0 = fully hidden
// Content
let content: Content
init(_ showSheet: Binding<Bool>, #ViewBuilder content: #escaping () -> Content) {
self._showSheet = showSheet
self.content = content()
}
var body: some View {
GeometryReader { geometry in
ZStack {
content
.blur(radius: 5 * dragPercentage)
.opacity(1 - dragPercentage * 0.5)
.disabled(showSheet)
.scaleEffect(1 - 0.1 * dragPercentage)
.frame(width: geometry.size.width, height: geometry.size.height)
if animatedSheet {
sheet.view
.background(Color.greyB.opacity(0.5).edgesIgnoringSafeArea(.bottom))
.cornerRadius(5)
.transition(.move(edge: .bottom).combined(with: .opacity))
.dragToSnap(snapPercentage: 0.3, dragPercentage: $dragPercentage) { showSheet = false } /// Custom modifier for measuring how far the view is dragged down. If more than 30% it snaps showSheet to false, and otherwise it snaps it back up again
.edgesIgnoringSafeArea(.bottom)
}
}
.onPreferenceChange(HomeOverlaySheet.self, perform: { value in self.sheet = value } )
.onChange(of: showSheet) { show in sheetUpdate(show) }
}
}
func sheetUpdate(_ show:Bool) {
withAnimation(.easeOut(duration: 0.2)) {
self.animatedSheet = show
if show { dragPercentage = 1 } else { dragPercentage = 0 }
}
// Delay onDismiss action if removing sheet, so animation can complete
if show == false {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
sheet.action()
}
}
}
}
struct HomeOverlays_Previews: PreviewProvider {
static var previews: some View {
HomeOverlays(.constant(false)) {
Text("Home overlays")
}
}
}
// MARK: Preference key for passing view up the tree
struct HomeOverlaySheet: PreferenceKey {
static var defaultValue: EquatableViewContainer = EquatableViewContainer(id: "default", view: AnyView(EmptyView()) )
static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) {
if value != nextValue() && nextValue().id != "default" {
value = nextValue()
}
}
}
// MARK: View extension for defining view somewhere in view tree
extension View {
// Change only leading view
func homeSheet<SheetView: View>(onDismiss action: #escaping () -> Void, #ViewBuilder sheet: #escaping () -> SheetView) -> some View {
let sheet = sheet()
return
self
.preference(key: HomeOverlaySheet.self, value: EquatableViewContainer(view: AnyView( sheet ), action: action ))
}
}

SwiftUI SearchBar problem with NavigationLink

I've a problem in SwiftUI with the searchBar appear.
There's a delay on its appear when I use NavigationLink. I saw that the problem appears only with NavigationLinks, if I use a conditional overlay or others "handmade" way to move between Views the problem doesn't appear. You know what I could do to fix the problem?
Here's my views code:
import SwiftUI
struct ContentView: View {
#State var searchText = ""
var body: some View {
NavigationView{
NavigationLink(destination: ContentView2()){
Text("Go to Sub View")
}
.navigationBarTitle("Main View")
.add(SearchBar(text: self.$searchText, hide: true, placeholder: "Search", cancelButton: true, autocapitalization: .sentences))
}
}
}
struct ContentView2 : View {
#State var searchText = ""
var body: some View {
Text("Hello, world!")
.navigationBarTitle("Sub View")
.add(SearchBar(text: self.$searchText, hide: true, placeholder: "Search", cancelButton: true, autocapitalization: .sentences))
}
}
My SearchBar code
import SwiftUI
class SearchBar: NSObject, ObservableObject {
let searchController: UISearchController = UISearchController(searchResultsController: nil)
#Binding var text: String
let hide : Bool
let placeholder : String
let cancelButton : Bool
let autocapitalization : UITextAutocapitalizationType
init(text: Binding<String>, hide: Bool, placeholder: String, cancelButton: Bool, autocapitalization: UITextAutocapitalizationType) {
self._text = text
self.hide = hide
self.placeholder = placeholder
self.cancelButton = cancelButton
self.autocapitalization = autocapitalization
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
self.searchController.hidesNavigationBarDuringPresentation = hide
self.searchController.automaticallyShowsCancelButton = cancelButton
self.searchController.searchBar.placeholder = placeholder
self.searchController.searchBar.autocapitalizationType = autocapitalization
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
My ViewController code
import SwiftUI
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
override func viewDidAppear(_ animated: Bool) {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
self.parent?.definesPresentationContext = true
self.parent?.navigationController?.navigationBar.sizeToFit()
}
override func viewDidDisappear(_ animated: Bool) {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
self.parent?.definesPresentationContext = true
self.parent?.navigationController?.navigationBar.sizeToFit()
}
}
And here's a video of the problem
Set the hidesSearchBarWhenScrolling property before the SearchBar is displayed on the screen. This can be done in viewWillAppear or as in the example below:
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}

Conflict using List/ScrollView with SFSafariViewController fullscreen sheet

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)
}
}

CoreData and FetchRequest, Issue with ForEach and "Cannot invoke initializer"

I just migrated to use CoreDate instead of a simple collection. I'm using iOS13 beta 8 and Xcode11 beta 6.
struct BeaconList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: fetchRequest(), animation: nil) var beacons: FetchedResults<Beacon>
static func fetchRequest() -> NSFetchRequest<Beacon> {
let request: NSFetchRequest<Beacon> = Beacon.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
return request
}
func buildView(name: String, beacons: [Beacon]) -> AnyView {
return AnyView (
Section(header: Text(name)) {
ForEach(beacons) { beacon in
BeaconListEntry(beacon: beacon)
}
}
)
}
var body: some View {
TabView {
NavigationView {
List {
buildView(name: "MY BEACONS", beacons: beacons.filter { $0.isActive })
}
.navigationBarTitle("Beacons")
.listStyle(GroupedListStyle())
.navigationBarItems(trailing: addButton)
}
.tabItem {
Image(systemName: "antenna.radiowaves.left.and.right")
Text("Beacons")
}
}
}
and with BeaconListEntryas follows:
struct BeaconListEntry : View {
#Binding var beacon: Beacon
var body: some View {
HStack {
Text(verbatim: beacon.name!)
}
}
}
(Please ignore the forced unwrapped, it's just for testing purposes)
When I used this with collections before I rewrote, it worked, but now I get the message
Cannot invoke initializer for type 'ForEach<_, _, _>' with an argument list of type '([Beacon], #escaping (Binding<Beacon>) -> BeaconListEntry)'
and
1. Overloads for 'ForEach<_, _, _>' exist with these partially matching parameter lists: (Data, content: #escaping (Data.Element) -> Content), (Range<Int>, content: #escaping (Int) -> Content)
Any idea on where to look? Is this the correct way to use FetchedResults?
The BeaconListEntry initializer expects an argument having type Binding, so you should modify your call as follows:
BeaconListEntry(beacon: .constant(beacon))