UIScrollView in SwiftUI, issues navigating using UIHostingController - ios

I need the pull to refresh functionality from a UIScrollView in SwiftUI. So I have a rudimentary UIViewRepresentable adaption of the UIKit component into SwiftUI. But by using the UIHostingController, I'm not able to navigate off of the screen as I'm guessing it manages navigation now. Looking at Apple's documentation, UIHostingController is meant to be used to bring SwiftUI views into UIKit, not the other way around. But removing UIHostingController from RefreshableScrollView doesn't render the ScrollView on the screen.
Is there any way for me to successfully use UIScrollView in SwiftUI without navigation issues or without UIHostingController?
// ContentView.swift
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geometry in
RefreshableScrollView(size: geometry.size, refresh: self.refresh) {
Text("Placeholder text")
}
}
}
NavigationLink(destination: Text("Destination")) {
Text("Click me")
}
}
func refresh() {
print("refreshing")
}
}
// RefreshableScrollView.swift
import SwiftUI
import UIKit
struct RefreshableScrollView<Content: View>: UIViewRepresentable {
var width: CGFloat
var height: CGFloat
var refresh: () -> Void
var refreshViewController: UIHostingController<Content>
init(size: CGSize, refresh: #escaping () -> Void, #ViewBuilder content: () -> Content) {
self.width = size.width
self.height = size.height
self.refresh = refresh
self.refreshViewController = UIHostingController(rootView: content())
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.refreshControl = UIRefreshControl()
scrollView.refreshControl?.addTarget(
context.coordinator,
action: #selector(Coordinator.handleRefreshControl(sender:)),
for: .valueChanged
)
refreshViewController.view.frame = CGRect(x: 0, y: 0, width: self.width, height: self.height)
scrollView.addSubview(self.refreshViewController.view)
return scrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var refreshScrollView: RefreshableScrollView
init(_ refreshScrollView: RefreshableScrollView) {
self.refreshScrollView = refreshScrollView
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
self.refreshScrollView.refresh()
sender.endRefreshing()
}
}
}

NavigationLink works inside NavigationView (except watchOS), so embed it as below
var body: some View {
NavigationView {
VStack {
GeometryReader { geometry in
RefreshableScrollView(size: geometry.size, refresh: self.refresh) {
Text("Placeholder text")
}
}
NavigationLink(destination: Text("Destination")) {
Text("Click me")
}
}
}
}

Related

SwiftUI: Notification when .contextMenu is dismissed (iOS)

I'm using .contextMenu together with .onDrag on a view and this seems to be very tricky:
The background color changes to gray by setting dragging to true. This is triggered by .onDrag which already happens when opening the context menu (a bit early but ok). When I use the button to close the menu I can set dragging to false. When I use the drag, the dragging state is changed back to false when the ItemProvider is deinitialized. So far so good.
The problem
When I tap outside the context menu to dismiss it, I seem to have no way to set the dragging state back to false. Adding .onDisappear to the Button in the menu does not work.
What am I doing wrong here? Any way I can get either get notified when the context menu closes or have the state change of dragging happen when the drag actually begins (so that the background is not immediately gray when the context menu is opened)?
Code below video.
struct ContentView: View {
#State var dragging = false
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: 100, height: 100)
.onDrag {
dragging = true
let provider = ItemProvider(contentsOf: URL(string: "Test")!)!
provider.didEnd = {
DispatchQueue.main.async {
dragging = false
}
}
print("init ItemProvider")
return provider
}
.contextMenu {
Button("Close Menu") {
dragging = false
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(dragging ? Color.gray : Color.white)
}
}
class ItemProvider: NSItemProvider {
var didEnd: (() -> Void)?
deinit {
print("deinit ItemProvider")
didEnd?()
}
}
EDIT (Dec. 2022): It seems like the code works in iOS 16.2. I still haven't found a good solution to this for earlier iOS versions.
Since you mention UIKit, .contextMenu is a UIContextMenuInteraction
You can add a UIContextMenuInteraction to a SwiftUI View and have access to UIContextMenuInteractionDelegate to identify when the menu is dismissed.
SwiftUI View > ViewModifier > UIViewRepresentable > Coordinator/UIContextMenuInteractionDelegate
struct CustomContextMenuView: View {
#State var dragging = false
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: 100, height: 100)
.onDrag {
dragging = true
let provider = ItemProvider(contentsOf: URL(string: "Test")!)!
provider.didEnd = {
DispatchQueue.main.async {
dragging = false
}
}
print("init ItemProvider")
return provider
}
//Use custom context menu and add actions as [UIAction]
.contextMenu(actions: [
UIAction(title: "Close Menu", handler: { a in
print("Close Menu action")
dragging = false
})
], willEnd: {
//Called when the menu is dismissed
print("willEnd/onDismiss")
dragging = false
}, willDisplay: {
//Called when the menu appears
print("willDisplay/onAppear")
})
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(dragging ? Color.gray : Color.white)
}
}
The Menu can be implemented by pasting the code below in a .swift file and using it like the sample above.
extension View {
func contextMenu(actions: [UIAction], willEnd: (() -> Void)? = nil, willDisplay: (() -> Void)? = nil) -> some View {
modifier(ContextMenuViewModifier(actions: actions, willEnd: willEnd, willDisplay: willDisplay))
}
}
struct ContextMenuViewModifier: ViewModifier {
let actions: [UIAction]
let willEnd: (() -> Void)?
let willDisplay: (() -> Void)?
func body(content: Content) -> some View {
Interaction_UI(view: {content}, actions: actions, willEnd: willEnd, willDisplay: willDisplay)
.fixedSize()
}
}
struct Interaction_UI<Content2: View>: UIViewRepresentable{
typealias UIViewControllerType = UIView
#ViewBuilder var view: Content2
let actions: [UIAction]
let willEnd: (() -> Void)?
let willDisplay: (() -> Void)?
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
func makeUIView(context: Context) -> some UIView {
let v = UIHostingController(rootView: view).view!
context.coordinator.contextMenu = UIContextMenuInteraction(delegate: context.coordinator)
v.addInteraction(context.coordinator.contextMenu!)
return v
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
class Coordinator: NSObject, UIContextMenuInteractionDelegate{
var contextMenu: UIContextMenuInteraction!
let parent: Interaction_UI
init(parent: Interaction_UI) {
self.parent = parent
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [self]
suggestedActions in
return UIMenu(title: "", children: parent.actions)
})
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
print(#function)
parent.willDisplay?()
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
print(#function)
parent.willEnd?()
}
}
}
struct ContentView: View {
#State var dragging = false
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: 100, height: 100)
.onDrag {
dragging = true
let provider = ItemProvider(contentsOf: URL(string: "Test")!)!
provider.didEnd = {
DispatchQueue.main.async {
dragging = false
}
}
print("init ItemProvider")
return provider
}
.contextMenu {
Button("Close Menu") {
dragging = false
}
}
.onTapGesture {
dragging = false
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(dragging ? Color.gray : Color.white)
}
}
class ItemProvider: NSItemProvider {
var didEnd: (() -> Void)?
deinit {
print("deinit ItemProvider")
didEnd?()
}
}

SwiftUI: Translucent background for fullScreenCover

So technically I want to show a loading screen view. I'm using fullScreenCover.
struct ContentView: View {
#State private var isLoading = false
var body: some View {
VStack {
Text("Hello there")
Button("Start loading") {
isLoading.toggle()
}
.fullScreenCover(isPresented: $isLoading) {
ZStack{
Color.black.opacity(0.5).edgesIgnoringSafeArea(.all)
VStack {
ProgressView()
Button("Stop loading") {
isLoading.toggle()
}
}
}
}
}
}
}
The problem is that I cannot make this loading screen translucent. sheet or popover behave the same way.
Here is a demo of possible way. Parameters of visual effect you can tune for your needs.
Tested with Xcode 12 / iOS 14.
// ... other code
.fullScreenCover(isPresented: $isLoading) {
ZStack{
Color.black.opacity(0.5).edgesIgnoringSafeArea(.all)
VStack {
ProgressView()
Button("Stop loading") {
isLoading.toggle()
}
}
}
.background(BackgroundBlurView())
}
}
}
}
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Building on top of f3dm76 answer:
I changed it so content behind would not flicker (that happened to me with lazy images loading behind fullScreenCover). Also I wanted to use custom transitions for full screen content (or in some cases no animation at all), so I removed default animations with this approach as well.
extension View {
func transparentNonAnimatingFullScreenCover<Content: View>(isPresented: Binding<Bool>, content: #escaping () -> Content) -> some View {
modifier(TransparentNonAnimatableFullScreenModifier(isPresented: isPresented, fullScreenContent: content))
}
}
private struct TransparentNonAnimatableFullScreenModifier<FullScreenContent: View>: ViewModifier {
#Binding var isPresented: Bool
let fullScreenContent: () -> (FullScreenContent)
func body(content: Content) -> some View {
content
.onChange(of: isPresented) { isPresented in
UIView.setAnimationsEnabled(false)
}
.fullScreenCover(isPresented: $isPresented,
content: {
ZStack {
fullScreenContent()
}
.background(FullScreenCoverBackgroundRemovalView())
.onAppear {
if !UIView.areAnimationsEnabled {
UIView.setAnimationsEnabled(true)
}
}
.onDisappear {
if !UIView.areAnimationsEnabled {
UIView.setAnimationsEnabled(true)
}
}
})
}
}
private struct FullScreenCoverBackgroundRemovalView: UIViewRepresentable {
private class BackgroundRemovalView: UIView {
override func didMoveToWindow() {
super.didMoveToWindow()
superview?.superview?.backgroundColor = .clear
}
}
func makeUIView(context: Context) -> UIView {
return BackgroundRemovalView()
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Asperi's answer is beautiful, but in case you want background to be transparent and not blurred, here is how you can modify it. I also moved the code into a modifier for convenience. (xcode 13.3, iOS 15.4.1)
extension View {
func transparentFullScreenCover<Content: View>(isPresented: Binding<Bool>, content: #escaping () -> Content) -> some View {
fullScreenCover(isPresented: isPresented) {
ZStack {
content()
}
.background(TransparentBackground())
}
}
}
struct TransparentBackground: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
I found this cleaner solution for the flicker issue in the clear background.
struct ClearBackgroundView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
return InnerView()
}
func updateUIView(_ uiView: UIView, context: Context) {
}
private class InnerView: UIView {
override func didMoveToWindow() {
super.didMoveToWindow()
superview?.superview?.backgroundColor = .clear
}
}
}
Usage
PresenterView()
.fullScreenCover(isPresented: $isPresented) {
PresentedView()
.background(ClearBackgroundView())
}
You can also use the Material background type:
ZStack{
...
}
.background(.ultraThinMaterial)
See documentation for more usage:
https://developer.apple.com/documentation/swiftui/material

How can I implement PageView in SwiftUI?

I am new to SwiftUI. I have three views and I want them in a PageView. I want to move each Views by swipe like a pageview and I want the little dots to indicate in which view I'm in.
iOS 15+
In iOS 15 a new TabViewStyle was introduced: CarouselTabViewStyle (watchOS only).
Also, we can now set styles more easily:
.tabViewStyle(.page)
iOS 14+
There is now a native equivalent of UIPageViewController in SwiftUI 2 / iOS 14.
To create a paged view, add the .tabViewStyle modifier to TabView and pass PageTabViewStyle.
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TabView {
FirstView()
SecondView()
ThirdView()
}
.tabViewStyle(PageTabViewStyle())
}
}
}
You can also control how the paging dots are displayed:
// hide paging dots
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
You can find a more detailed explanation in this link:
How to create scrolling pages of content using tabViewStyle()
Vertical variant
TabView {
Group {
FirstView()
SecondView()
ThirdView()
}
.rotationEffect(Angle(degrees: -90))
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.rotationEffect(Angle(degrees: 90))
Custom component
If you're tired of passing tabViewStyle every time you can create your own PageView:
Note: TabView selection in iOS 14.0 worked differently and that's why I used two Binding properties: selectionInternal and selectionExternal. As of iOS 14.3 it seems to be working with just one Binding. However, you can still access the original code from the revision history.
struct PageView<SelectionValue, Content>: View where SelectionValue: Hashable, Content: View {
#Binding private var selection: SelectionValue
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private let indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode
private let content: () -> Content
init(
selection: Binding<SelectionValue>,
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
#ViewBuilder content: #escaping () -> Content
) {
self._selection = selection
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
var body: some View {
TabView(selection: $selection) {
content()
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: indexBackgroundDisplayMode))
}
}
extension PageView where SelectionValue == Int {
init(
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
#ViewBuilder content: #escaping () -> Content
) {
self._selection = .constant(0)
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
}
Now you have a default PageView:
PageView {
FirstView()
SecondView()
ThirdView()
}
which can be customised:
PageView(indexDisplayMode: .always, indexBackgroundDisplayMode: .always) { ... }
or provided with a selection:
struct ContentView: View {
#State var selection = 1
var body: some View {
VStack {
Text("Selection: \(selection)")
PageView(selection: $selection, indexBackgroundDisplayMode: .always) {
ForEach(0 ..< 3, id: \.self) {
Text("Page \($0)")
.tag($0)
}
}
}
}
}
Page Control
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
control.pageIndicatorTintColor = UIColor.lightGray
control.currentPageIndicatorTintColor = UIColor.darkGray
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
return control
}
func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}
class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
#objc
func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
}
Your page View
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
ZStack(alignment: .bottom) {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
}
}
}
Your page View Controller
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
#State private var previousPage = 0
init(controllers: [UIViewController],
currentPage: Binding<Int>)
{
self.controllers = controllers
self._currentPage = currentPage
self.previousPage = currentPage.wrappedValue
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
guard !controllers.isEmpty else {
return
}
let direction: UIPageViewController.NavigationDirection = previousPage < currentPage ? .forward : .reverse
context.coordinator.parent = self
pageViewController.setViewControllers(
[controllers[currentPage]], direction: direction, animated: true) { _ in {
previousPage = currentPage
}
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
Let's say you have a view like
struct CardView: View {
var album: Album
var body: some View {
URLImage(URL(string: album.albumArtWork)!)
.resizable()
.aspectRatio(3 / 2, contentMode: .fit)
}
}
You can use this component in your main SwiftUI view like this.
PageView(vM.Albums.map { CardView(album: $0) }).frame(height: 250)
iOS 13+ (private API)
Warning: The following answer uses private SwiftUI methods that aren't publicly visible (you can still access them if you know where to look). However, they are not documented properly and may be unstable. Use them at your own risk.
While browsing SwiftUI files I stumbled upon the _PagingView that seems to be available since iOS 13:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingView<Views> : SwiftUI.View where Views : Swift.RandomAccessCollection, Views.Element : SwiftUI.View, Views.Index : Swift.Hashable
This view has two initialisers:
public init(config: SwiftUI._PagingViewConfig = _PagingViewConfig(), page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
public init(direction: SwiftUI._PagingViewConfig.Direction, page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
What we also have is the _PagingViewConfig:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingViewConfig : Swift.Equatable {
public enum Direction {
case vertical
case horizontal
public static func == (a: SwiftUI._PagingViewConfig.Direction, b: SwiftUI._PagingViewConfig.Direction) -> Swift.Bool
public var hashValue: Swift.Int {
get
}
public func hash(into hasher: inout Swift.Hasher)
}
public var direction: SwiftUI._PagingViewConfig.Direction
public var size: CoreGraphics.CGFloat?
public var margin: CoreGraphics.CGFloat
public var spacing: CoreGraphics.CGFloat
public var constrainedDeceleration: Swift.Bool
public init(direction: SwiftUI._PagingViewConfig.Direction = .horizontal, size: CoreGraphics.CGFloat? = nil, margin: CoreGraphics.CGFloat = 0, spacing: CoreGraphics.CGFloat = 0, constrainedDeceleration: Swift.Bool = true)
public static func == (a: SwiftUI._PagingViewConfig, b: SwiftUI._PagingViewConfig) -> Swift.Bool
}
Now, we can create a simple _PagingView:
_PagingView(direction: .horizontal, views: [
AnyView(Color.red),
AnyView(Text("Hello world")),
AnyView(Rectangle().frame(width: 100, height: 100))
])
Here is another, more customised example:
struct ContentView: View {
#State private var selection = 1
var body: some View {
_PagingView(
config: _PagingViewConfig(
direction: .vertical,
size: nil,
margin: 10,
spacing: 10,
constrainedDeceleration: false
),
page: $selection,
views: [
AnyView(Color.red),
AnyView(Text("Hello world")),
AnyView(Rectangle().frame(width: 100, height: 100))
]
)
}
}
For apps that target iOS 14 and later, the answer suggested by #pawello2222 should be considered the correct one. I have tried it in two apps now and it works great, with very little code.
I have wrapped the proposed concept in a struct that can be provided with both views as well as with an item list and a view builder. It can be found here. The code looks like this:
#available(iOS 14.0, *)
public struct MultiPageView: View {
public init<PageType: View>(
pages: [PageType],
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
currentPageIndex: Binding<Int>) {
self.pages = pages.map { AnyView($0) }
self.indexDisplayMode = indexDisplayMode
self.currentPageIndex = currentPageIndex
}
public init<Model, ViewType: View>(
items: [Model],
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
currentPageIndex: Binding<Int>,
pageBuilder: (Model) -> ViewType) {
self.pages = items.map { AnyView(pageBuilder($0)) }
self.indexDisplayMode = indexDisplayMode
self.currentPageIndex = currentPageIndex
}
private let pages: [AnyView]
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private var currentPageIndex: Binding<Int>
public var body: some View {
TabView(selection: currentPageIndex) {
ForEach(Array(pages.enumerated()), id: \.offset) {
$0.element.tag($0.offset)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
}
}
first you adds the package https://github.com/xmartlabs/PagerTabStripView
then
import SwiftUI
import PagerTabStripView
struct MyPagerView: View {
var body: some View {
PagerTabStripView() {
FirstView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "ACCOUNT", systomIcon: "character.bubble.fill")
}
ContentView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "PROFILE", systomIcon: "person.circle.fill")
}
NewsAPIView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "PASSWORD", systomIcon: "lock.fill")
}
}
.pagerTabStripViewStyle(.barButton(indicatorBarHeight: 4, indicatorBarColor: .black, tabItemSpacing: 0, tabItemHeight: 90))
}
}
struct TitleNavBarItem: View {
let title: String
let systomIcon: String
var body: some View {
VStack {
Image(systemName: systomIcon)
.foregroundColor( .white)
.font(.title)
Text( title)
.font(.system(size: 22))
.bold()
.foregroundColor(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.orange)
}
}
The easiest way to do this is via iPages.
import SwiftUI
import iPages
struct ContentView: View {
#State var currentPage = 0
var body: some View {
iPages(currentPage: $currentPage) {
Text("😋")
Color.pink
}
}
}

Change popover size in SwiftUI

I'm trying to set a certain size for a popover or to make it adapt its content
I tried to change the frame for the view from popover, but it does not seem to work
Button("Popover") {
self.popover7.toggle()
}.popover(isPresented: self.$popover7, arrowEdge: .bottom) {
PopoverView().frame(width: 100, height: 100, alignment: .center)
}
I'd like to achieve this behaviour I found in Calendar app in iPad
The solution by #ccwasden works very well. I extended his work by making it more "natural" in terms of SwiftUI. Also, this version utilizes sizeThatFits method, so you don't have to specify the size of the popover content.
struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
#Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let content: () -> PopoverContent
func body(content: Content) -> some View {
content
.background(
Popover(
isPresented: self.$isPresented,
onDismiss: self.onDismiss,
content: self.content
)
)
}
}
extension View {
func popover<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
content: #escaping () -> Content
) -> some View where Content: View {
ModifiedContent(
content: self,
modifier: PopoverViewModifier(
isPresented: isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
struct Popover<Content: View> : UIViewControllerRepresentable {
#Binding var isPresented: Bool
let onDismiss: (() -> Void)?
#ViewBuilder let content: () -> Content
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self, content: self.content())
}
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.host.rootView = self.content()
if self.isPresented, uiViewController.presentedViewController == nil {
let host = context.coordinator.host
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: Int.max, height: Int.max))
host.modalPresentationStyle = UIModalPresentationStyle.popover
host.popoverPresentationController?.delegate = context.coordinator
host.popoverPresentationController?.sourceView = uiViewController.view
host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
uiViewController.present(host, animated: true, completion: nil)
}
}
class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
let host: UIHostingController<Content>
private let parent: Popover
init(parent: Popover, content: Content) {
self.parent = parent
self.host = UIHostingController(rootView: content)
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
self.parent.isPresented = false
if let onDismiss = self.parent.onDismiss {
onDismiss()
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
}
I got it to work on iOS with a custom UIViewRepresentable. Here is what the usage looks like:
struct Content: View {
#State var open = false
#State var popoverSize = CGSize(width: 300, height: 300)
var body: some View {
WithPopover(
showPopover: $open,
popoverSize: popoverSize,
content: {
Button(action: { self.open.toggle() }) {
Text("Tap me")
}
},
popoverContent: {
VStack {
Button(action: { self.popoverSize = CGSize(width: 300, height: 600)}) {
Text("Increase size")
}
Button(action: { self.open = false}) {
Text("Close")
}
}
})
}
}
And here is a gist with the source for WithPopover
macOS-only
Here is how to change frame of popover dynamically... for simplicity it is w/o animation, it is up to you.
struct TestCustomSizePopover: View {
#State var popover7 = false
var body: some View {
VStack {
Button("Popover") {
self.popover7.toggle()
}.popover(isPresented: self.$popover7, arrowEdge: .bottom) {
PopoverView()
}
}.frame(width: 800, height: 600)
}
}
struct PopoverView: View {
#State var adaptableHeight = CGFloat(100)
var body: some View {
VStack {
Text("Popover").padding()
Button(action: {
self.adaptableHeight = 300
}) {
Text("Button")
}
}
.frame(width: 100, height: adaptableHeight)
}
}

How to handle NavigationLink inside UIViewControllerRepresentable wrapper?

So I am trying to create a custom pagination scrollView. I have been able to create that wrapper and the content inside that wrapper consists of a custom View. Inside that custom View i have got two NavigationLink buttons when pressed should take users to two different Views.
Those NavigationLink buttons are not working.
The scrollViewWrapper is inside a NavigationView. I created a test button which is just a simple Button and that seems to work. So there is something that I am not doing correctly with NavigationLink and custom UIViewControllerRepresentable.
This is where I am using the custom wrapper.
NavigationView {
UIScrollViewWrapper {
HStack(spacing: 0) {
ForEach(self.onboardingDataArray, id: \.id) { item in
OnboardingView(onboardingData: item)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.blue)
}
The onboarding view:
struct OnboardingView: View {
var onboardingData: OnboardingModel
var body: some View {
GeometryReader { geometry in
VStack(spacing: 10) {
Spacer()
Image("\(self.onboardingData.image)")
.resizable()
.frame(width: 300, height: 300)
.aspectRatio(contentMode: ContentMode.fit)
.clipShape(Circle())
.padding(20)
Text("\(self.onboardingData.titleText)")
.frame(width: geometry.size.width, height: 20, alignment: .center)
.font(.title)
Text("\(self.onboardingData.descriptionText)")
.lineLimit(nil)
.padding(.leading, 15)
.padding(.trailing, 15)
.font(.system(size: 16))
.frame(width: geometry.size.width, height: 50, alignment: .center)
.multilineTextAlignment(.center)
Spacer(minLength: 20)
if self.onboardingData.showButton ?? false {
VStack {
Button(action: {
print("Test")
}) {
Text("Test Button")
}
NavigationLink(destination: LogInView()) {
Text("Login!")
}
NavigationLink(destination: SignUpView()) {
Text("Sign Up!")
}
}
}
Spacer()
}
}
}
}
The custom ScrollView Wrapper code:
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewController {
let vc = UIScrollViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
class UIScrollViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.isPagingEnabled = true
return view
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
As the other answers have stated, there is an issue with putting the NavigationLink inside the UIViewControllerRepresentable.
I solved this by wrapping my UIViewControllerRepresentable and a NavigationLink inside a View and programmatically activating the NavigationLink from inside the UIViewControllerRepresentable.
For example:
struct MyView: View
{
#State var destination: AnyView? = nil
#State var is_active: Bool = false
var body: some View
{
ZStack
{
MyViewControllerRepresentable( self )
NavigationLink( destination: self.destination, isActive: self.$is_active )
{
EmptyView()
}
}
}
func goTo( destination: AnyView )
{
self.destination = destination
self.is_active = true
}
}
In my case, I passed the MyView instance to the UIViewController that my MyViewControllerRepresentable is wrapping, and called my goTo(destination:AnyView) method when a button was clicked.
The difference between our cases is that my UIViewController was my own class written with UIKit (compared to a UIHostingController). In the case that you're using a UIHostingController, you could probably use a shared ObservableObject containing the destination and is_active variables. You'd change your 2 NavigationLinks to Buttons having the action methods change the ObservableObject's destination and is_active variables.
This happens because you use a UIViewControllerRepresentable instead of UIViewRepresentable. I guess the UIScrollViewController keeps the destination controller from being presented by the current controller.
Try the code above instead:
import UIKit
import SwiftUI
struct ScrollViewWrapper<Content>: UIViewRepresentable where Content: View{
func updateUIView(_ uiView: UIKitScrollView, context: UIViewRepresentableContext<ScrollViewWrapper<Content>>) {
}
typealias UIViewType = UIKitScrollView
let content: () -> Content
var showsIndicators : Bool
public init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.showsIndicators = showsIndicators
}
func makeUIView(context: UIViewRepresentableContext<ScrollViewWrapper>) -> UIViewType {
let hosting = UIHostingController(rootView: AnyView(content()))
let width = UIScreen.main.bounds.width
let size = hosting.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
hosting.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
let view = UIKitScrollView()
view.delegate = view
view.alwaysBounceVertical = true
view.addSubview(hosting.view)
view.contentSize = CGSize(width: width, height: size.height)
return view
}
}
class UIKitScrollView: UIScrollView, UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset) // Do whatever you want.
}
}
This is an extension to the above solution that never scrolls the inner content.
I was into a similar problem. I have figured out that the problem is with the UIViewControllerRepresentable. Instead use UIViewRepresentable, although I am not sure what the issue is. I was able to get the navigationlink work using the below code.
struct SwiftyUIScrollView<Content>: UIViewRepresentable where Content: View {
typealias UIViewType = Scroll
var content: () -> Content
var pagingEnabled: Bool = false
var hideScrollIndicators: Bool = false
#Binding var shouldUpdate: Bool
#Binding var currentIndex: Int
var onScrollIndexChanged: ((_ index: Int) -> Void)
public init(pagingEnabled: Bool,
hideScrollIndicators: Bool,
currentIndex: Binding<Int>,
shouldUpdate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content, onScrollIndexChanged: #escaping ((_ index: Int) -> Void)) {
self.content = content
self.pagingEnabled = pagingEnabled
self._currentIndex = currentIndex
self._shouldUpdate = shouldUpdate
self.hideScrollIndicators = hideScrollIndicators
self.onScrollIndexChanged = onScrollIndexChanged
}
func makeUIView(context: UIViewRepresentableContext<SwiftyUIScrollView>) -> UIViewType {
let hosting = UIHostingController(rootView: content())
let view = Scroll(hideScrollIndicators: hideScrollIndicators, isPagingEnabled: pagingEnabled)
view.scrollDelegate = context.coordinator
view.alwaysBounceHorizontal = true
view.addSubview(hosting.view)
makefullScreen(of: hosting.view, to: view)
return view
}
class Coordinator: NSObject, ScrollViewDelegate {
func didScrollToIndex(_ index: Int) {
self.parent.onScrollIndexChanged(index)
}
var parent: SwiftyUIScrollView
init(_ parent: SwiftyUIScrollView) {
self.parent = parent
}
}
func makeCoordinator() -> SwiftyUIScrollView<Content>.Coordinator {
Coordinator(self)
}
func updateUIView(_ uiView: Scroll, context: UIViewRepresentableContext<SwiftyUIScrollView<Content>>) {
if shouldUpdate {
uiView.scrollToIndex(index: currentIndex)
}
}
func makefullScreen(of childView: UIView, to parentView: UIView) {
childView.translatesAutoresizingMaskIntoConstraints = false
childView.leftAnchor.constraint(equalTo: parentView.leftAnchor).isActive = true
childView.rightAnchor.constraint(equalTo: parentView.rightAnchor).isActive = true
childView.topAnchor.constraint(equalTo: parentView.topAnchor).isActive = true
childView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).isActive = true
}
}
Then create a new class to handle the delegates of a scrollview. You can include the below code into the UIViewRepresentable as well. But I prefer keeping it separated for a clean code.
class Scroll: UIScrollView, UIScrollViewDelegate {
var hideScrollIndicators: Bool = false
var scrollDelegate: ScrollViewDelegate?
var tileWidth = 270
var tileMargin = 20
init(hideScrollIndicators: Bool, isPagingEnabled: Bool) {
super.init(frame: CGRect.zero)
showsVerticalScrollIndicator = !hideScrollIndicators
showsHorizontalScrollIndicator = !hideScrollIndicators
delegate = self
self.isPagingEnabled = isPagingEnabled
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
scrollDelegate?.didScrollToIndex(Int(currentIndex))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
scrollDelegate?.didScrollToIndex(Int(currentIndex))
}
func scrollToIndex(index: Int) {
let newOffSet = CGFloat(tileWidth+tileMargin) * CGFloat(index)
contentOffset = CGPoint(x: newOffSet, y: contentOffset.y)
}
}
Now to implement the scrollView use the below code.
#State private var activePageIndex: Int = 0
#State private var shouldUpdateScroll: Bool = false
SwiftyUIScrollView(pagingEnabled: false, hideScrollIndicators: true, currentIndex: $activePageIndex, shouldUpdate: $shouldUpdateScroll, content: {
HStack(spacing: 20) {
ForEach(self.data, id: \.id) { data in
NavigationLink(destination: self.getTheNextView(data: data)) {
self.cardView(data: data)
}
}
}
.padding(.horizontal, 30.0)
}, onScrollIndexChanged: { (newIndex) in
shouldUpdateScroll = false
activePageIndex = index
// Your own required handling
})
func getTheNextView(data: Any) -> AnyView {
// Return the required destination View
}
Don't forget to add your hosting controller as a child.
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
addChild(self.hostingController)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
Did you set up the Triggered Segues? If you are using Xcode you can right click the button you created in the main storyboard. If it's not set up you can go to the connections inspector on the top right sidebar where you can find the File inspector,Identity inspector, Attributes inspector... and specify the action of what you want your button to do.

Resources