Recreate Safaris Tab animation open opening / closing a WKWebView - swift

so I am trying to recreate that animation in Safari where you have a window open and then click the button to "view all tabs". And then when you tap on an individual window it animated open.
Here is a video of an example: https://imgur.com/QNI26YK
I'm using SwiftUI currently but think I might need to do it in UIKit? Anyways, i'll show you what I have so far.
The issue I have is the scaling of the window. In safari it scales so seamlessly, where when I do it you can see it readjust. It's like they just minimize the window in some way.
Anyways, here is what I have. Any ideas would be great! Thanks
struct WebViewAnimationTest: View {
#State var show = false
#Namespace var namespace
var body: some View {
ZStack {
SimpleWebView(url: "https://google.com", fullscreen: show)
.frame(width: show ? .infinity : 200, height: show ? .infinity : 300)
.overlay(Color.primary.opacity(0.01))
.ignoresSafeArea()
.onTapGesture {
withAnimation(.spring()) {
show.toggle()
}
}
.matchedGeometryEffect(id: "id", in: namespace)
}
}
}
WEBVIEW:
import Combine
import SwiftUI
import WebKit
struct SimpleWebView: UIViewRepresentable {
typealias UIViewType = WKWebView
var url: String
var fullscreen: Bool = false
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
let url = URL(string: url)
webView.load(URLRequest(url: url!))
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if fullscreen {
uiView.transform = CGAffineTransform(scaleX: 1, y: 1)
} else {
let actualWidth = (getRect().width - 60)
let cardWidth = actualWidth / 2
let scale = cardWidth / actualWidth
uiView.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
}

Here is what I have so far. You might want to switch to LazyVGrid with ScrollView. The idea is to animate changes in scale effect.
import SwiftUI
struct ContentView: View {
var body: some View {
WebViewAnimationTest()
}
}
struct WebViewAnimationTest: View {
#State var show = false
#Namespace var namespace
let data = SimpleWebView(url: "https://google.com")
var body: some View {
ScrollView {
ZStack {
HStack {
ForEach([data], id: \.id) { view in
view
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.ignoresSafeArea()
.scaleEffect(show ? 1.0 : 0.5)
.onTapGesture {
withAnimation {
show.toggle()
}
}
}
}
}
}
}
}
import WebKit
struct SimpleWebView: UIViewRepresentable {
typealias UIViewType = WKWebView
var url: String
var fullscreen: Bool = false
let id = UUID()
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
let url = URL(string: url)
webView.load(URLRequest(url: url!))
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

You can use 'UIView.transition' like below
let webView = WKWebView()
self.view.addSubview(webView)
UIView.transition(with: self.view, duration: 0.5, options: .transitionFlipFromRight, animations: {
webView.isHidden = false
}, completion: nil)
UIView.transition(with: self.view, duration: 0.5, options: .transitionCrossDissolve, animations: {
webView.isHidden = true
}, completion: nil)

Related

User input to change size of shape that is generated by tap

I'm creating an app in RealityKit that generates a shape based on user input. For example, if the user enters a radius of 0.1 meters, the shape (sphere in my case) will have a radius of 0.1 meters, same logic for 0.2, 0.3, etc. The code all works, but I want to make it so the sphere appears when the user taps the screen.
Here is my code for the page that takes in user input:
class UserInput: ObservableObject {
#Published var score: Float = 0.0
}
struct PreviewView: View {
#ObservedObject var input = UserInput()
var body: some View {
NavigationView {
ZStack {
Color.black
VStack {
Text("Change the radius of your AR sphere")
.foregroundColor(.white)
Text("\(String(format: "%.1f", self.input.score)) meters")
.foregroundColor(.white)
.bold()
.font(.title)
.padding(10)
Button(action: {self.input.score += 0.1})
{
Text("Increment by 0.1 meters")
}
.padding(10)
Button(action: {self.input.score -= 0.1})
{
Text("Decrease by 0.1 meters")
}
.padding(10)
NavigationLink(destination: Reality(input: self.input)) {
Text("View in AR")
.bold()
.padding(.top,30)
}
}
}
.ignoresSafeArea()
}
}
Here is the code for the Reality ARView:
struct Reality: UIViewRepresentable {
#ObservedObject var input: UserInput
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let model = ModelEntity(mesh: .generateSphere(radius: input.score))
let anchor = AnchorEntity(plane: .horizontal)
anchor.addChild(model)
arView.scene.anchors.append(anchor)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
There are tons of examples of generating shapes when the user touches the screen, that's not my issue. The fact that I'm taking in user input is what makes this difficult.
Here is some code that does what I want, but without user input. It has some physics built in that I plan on implementing once I get the user input to work.
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let planeAnchorEntity = AnchorEntity(plane: .horizontal)
let plane = ModelEntity(mesh: MeshResource.generatePlane(width: 1, depth: 1), materials: [SimpleMaterial(color: .white, isMetallic: true)])
plane.physicsBody = PhysicsBodyComponent(massProperties: .init(mass: 1), material: .generate(friction: 1, restitution: 1), mode: .kinematic)
plane.generateCollisionShapes(recursive: true)
planeAnchorEntity.addChild(plane)
arView.scene.anchors.append(planeAnchorEntity)
arView.installGestures([.scale,.rotation], for: plane)
arView.addGestureRecognizer(UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap)))
context.coordinator.view = arView
return arView
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
Here is the Coordinator class that generates the box that I want to be adjustable in size:
class Coordinator {
weak var view: ARView?
#objc func handleTap(_ recognizer: UITapGestureRecognizer) {
guard let view = view else { return }
let location = recognizer.location(in: view)
let results = view.raycast(from: location, allowing: .estimatedPlane, alignment: .horizontal)
if let result = results.first {
let anchorEntity = AnchorEntity(raycastResult: result)
let box = ModelEntity(mesh: MeshResource.generateBox(size: 0.3),materials: [SimpleMaterial(color: .black, isMetallic: true)])
box.physicsBody = PhysicsBodyComponent(massProperties: .init(mass: 0.5), material: .generate(), mode: .dynamic)
box.generateCollisionShapes(recursive: true)
box.position = simd_make_float3(0,0.7,0)
//static means body cannot be moved
//dynamic means it can move
//kinematic means user moved the object
anchorEntity.addChild(box)
view.scene.anchors.append(anchorEntity)
}
}
}
I tried fusing these two projects into what I want, but I get all sorts of errors I have no idea how to fix, and when I try something new, a bunch of other errors appear. I think it boils down to the #ObservedObject and the fact that I have multiple classes/structs compared my project with user input. The user input will go into the coordinator class, but ultimately it is the ARViewContainer that actually renders the view.
If anyone can help me out, I would be incredibly grateful.
To increase / decrease sphere's radius and then position sphere by tap, use the following code.
Now, with working button and coordinator, it will be much easier to implement a raycasting.
import SwiftUI
import RealityKit
struct ContentView : View {
var body: some View {
PrevView().ignoresSafeArea()
}
}
Reality view.
struct Reality: UIViewRepresentable {
#Binding var input: Float
let arView = ARView(frame: .zero)
class ARCoordinator: NSObject {
var manager: Reality
init(_ manager: Reality) {
self.manager = manager
super.init()
let recognizer = UITapGestureRecognizer(target: self,
action: #selector(tapped))
manager.arView.addGestureRecognizer(recognizer)
}
#objc func tapped(_ recognizer: UITapGestureRecognizer) {
if manager.arView.scene.anchors.isEmpty {
let model = ModelEntity(mesh: .generateSphere(radius:
manager.input))
let anchor = AnchorEntity(world: [0, 0,-2])
// later use AnchorEntity(world: result.worldTransform)
anchor.addChild(model)
manager.arView.scene.anchors.append(anchor)
}
}
}
func makeCoordinator() -> ARCoordinator { ARCoordinator(self) }
func makeUIView(context: Context) -> ARView { return arView }
func updateUIView(_ uiView: ARView, context: Context) { }
}
PrevView view.
struct PrevView: View {
#State private var input: Float = 0.0
var body: some View {
NavigationView {
ZStack {
Color.black
VStack {
Text("\(String(format: "%.1f", input)) meters")
.foregroundColor(.yellow)
HStack {
Spacer()
Button(action: { $input.wrappedValue -= Float(0.1) }) {
Text("Decrease").foregroundColor(.red)
}
Spacer()
Button(action: { $input.wrappedValue += Float(0.1) }) {
Text("Increase").foregroundColor(.red)
}
Spacer()
}
NavigationLink(destination: Reality(input: $input)
.ignoresSafeArea()) {
Text("View in AR")
}
}
}.ignoresSafeArea()
}
}
}

SwiftUI playing Lottie animation

I was playing with the Lottie animations, these days, but I have some trouble how to stop them from the View.
Here is my ContentView, where I am playing the animation. I have one button which shows the boolean value in real-time:
struct ContentView: View {
#State var isShowing : Bool = true
var body: some View {
ZStack {
Color.green.opacity(0.3)
VStack {
VStack {
LottieDosi(isShowing: .constant(isShowing)){Text("")}
Button(action: {self.isShowing.toggle()}) {
Text("Button is \(String(isShowing))")
.padding(10)
.background(Color.white)
.cornerRadius(40)
}
}.padding(.horizontal, 30)
}
}.edgesIgnoringSafeArea(.all)
}
}
and here is my LottieView:
struct LottieView: UIViewRepresentable {
#Binding var isAnimating: Bool
#State var name : String
var loopMode: LottieLoopMode = .loop
var animationView = AnimationView()
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView()
animationView.animation = Animation.named(name)
animationView.contentMode = .scaleAspectFill
animationView.loopMode = .loop
animationView.play()
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
isAnimating ? animationView.play() : animationView.stop()
}
}
struct LottieDosi<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
VStack {
LottieView(isAnimating: .constant(isShowing), name: "13728-sticker-4")
.frame(width: 175, height: 175)
}
}
}
I have a binding bool which has to change itself either to true or false, whenever I tap on the button. In other words, when I press the button I should be able to play and stop it. But somehow, this boolean value is not shared between the structs and the animation is constantly playing. I am new to swift UI, so I assume I am doing something very wrong, therefore I will appreciate some help.
Here you can see my animation
You've almost got it. You need to construct a Coordinator so that you can access the values inside your LottieView. It's straight forward to do.
Create a Coordinator class that conforms to NSObject
Pass in your LottieView.
Add the function makeCoordinator to your LottieView
Create an instance of your Coordinator in the makeCoordinator function
In your updateUIView access the animationView from the coordinator.
You shouldn't need a binding for the isAnimating as the LottieView is not passing any information back to your ContentView. You also don't need that #State for the name
Here is how I have gotten Lottie to play and pause in my apps. Note that some of the variable names are different to yours but it should be enough to get you to where you need to be.
import SwiftUI
import Lottie
struct LottieView: UIViewRepresentable {
typealias UIViewType = UIView
let filename: String
let animationView = AnimationView()
let isPaused: Bool
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
let animation = Animation.named(filename)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor),
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
if isPaused {
context.coordinator.parent.animationView.pause()
} else {
context.coordinator.parent.animationView.play()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: LottieView
init(_ parent: LottieView) {
self.parent = parent
}
}
}
My ContentView looks like the following:
struct ContentView: View {
#State private var isPaused: Bool = true
var body: some View {
VStack {
LottieView(filename: "loading", isPaused: isPaused)
.frame(width: 200, height: 200)
Button(action: {
self.isPaused.toggle()
}, label: {
Text(isPaused ? "Play" : "Pause")
})
}
}
}
Here it is working:

Taking screenshot doesn't work when using #State in SwiftUI

I am trying to take a screenshot of the selected area using CGRect. It works fine if I don't use #State variable. But I need to use #State variable too.
Here is my code...
struct ScreenShotTest: View {
#State var abc = 0 //Works well if I remove the line
var body: some View {
Button(action: {
let image = self.takeScreenshot(theRect: CGRect(x: 0, y: 0, width: 200, height: 100))
print(image)
}) {
Text("Take Screenshot")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
}
}
}
extension UIView {
var renderedImage: UIImage {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
extension View {
func takeScreenshot(theRect: CGRect) -> UIImage {
let window = UIWindow(frame: theRect)
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
I ran into this problem too, but with an EnvironmentObject. I solved it by declaring the EnvironmentObject in an outer view and passing it as a plain variable to the inner view that was crashing when I used it. This is pared way down from my real code, but it should convey the idea.
struct Sample: View {
#State private var isSharePresented: Bool = false
#EnvironmentObject var model: Model
static var screenshot: UIImage?
var body: some View {
GeometryReader { geometry in
// Hack to get a black background.
ZStack {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.black)
.edgesIgnoringSafeArea(.all)
VStack {
CompletionContent(model: Model.theModel)
Spacer()
Button(action: {
model.advanceToNext()
}) {
Text("Continue", comment: "Dismiss this view")
.font(.headline)
.foregroundColor(.init(UIColor.link))
}
HStack {
Spacer()
Button(action: {
Sample.screenshot = self.takeScreenshot(theRect: (geometry.frame(in: .global)))
self.isSharePresented = true
}) {
VStack {
Image("blank")
Image(systemName: "square.and.arrow.up")
.resizable()
.frame(width: 20, height: 30)
}
}
}
Spacer()
}
}
}
.sheet(isPresented: $isSharePresented, onDismiss: {
print("Dismiss")
}, content: {
ActivityViewController(screenshot: Sample.screenshot!)
})
}
}
struct SampleContent : View {
var model: Model
var body: some View {
Text("Content that uses the model variable goes here")
}
}
extension UIView {
var renderedImage: UIImage {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
extension View {
func takeScreenshot(theRect: CGRect) -> UIImage {
let window = UIWindow(frame: theRect)
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
struct ActivityViewController: UIViewControllerRepresentable {
var screenshot: UIImage
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(
context: UIViewControllerRepresentableContext<ActivityViewController>)
-> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: [screenshot], applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {
}
}

Add New Window and make it key window in SWIFTUI

I want to add a new window as I want to create a a full screen loader. I have tried to add a new window in and set that as rootviewcontroller. But it is not adding into the windows hierarchy. Below is my code. I am learning swiftUI. Any help is appreciated.
let window = UIWindow()
window.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
window.backgroundColor = .blue
window.isHidden = false
window.rootViewController = UIHostingController(rootView: Text("Loading...."))
window.makeKeyAndVisible()
You need to wrap UIActivityIndicator and make it UIViewRepresentable.
struct ActivityIndicator: UIViewRepresentable {
#Binding var isAnimating: Bool
style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Then you can use it as follows – here’s an example of a loading overlay.
Note: I prefer using ZStack, rather than overlay(:_), so I know exactly what’s
going on in my implementation
struct LoadingView: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to show alternate window, you have to connect new UIWindow to existed window scene, so here is a demo of possible approach to do this in SceneDelegate, based on posted notifications.
// notification names declarations
let showFullScreenLoader = NSNotification.Name("showFullScreenLoader")
let hideFullScreenLoader = NSNotification.Name("hideFullScreenLoader")
// demo alternate window
struct FullScreenLoader: View {
var body: some View {
VStack {
Spacer()
Button("Close Loader") {
NotificationCenter.default.post(name: hideFullScreenLoader, object: nil)
}
}
}
}
// demo main window
struct MainView: View {
var body: some View {
VStack {
Button("Show Loader") {
NotificationCenter.default.post(name: showFullScreenLoader, object: nil)
}
Spacer()
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? // << main window
var loaderWindow: UIWindow? // << alternate window
private var subscribers = Set<AnyCancellable>()
func makeAntherWindow() { // << alternate window creation
if let windowScene = window?.windowScene {
let newWindow = UIWindow(windowScene: windowScene)
let contentView = FullScreenLoader()
newWindow.rootViewController = UIHostingController(rootView: contentView)
self.loaderWindow = newWindow
newWindow.makeKeyAndVisible()
}
}
override init() {
super.init()
NotificationCenter.default.publisher(for: hideFullScreenLoader)
.sink(receiveValue: { _ in
self.loaderWindow = nil // remove alternate window
})
.store(in: &self.subscribers)
NotificationCenter.default.publisher(for: showFullScreenLoader)
.sink(receiveValue: { _ in
self.makeAntherWindow() // create alternate window
})
.store(in: &self.subscribers)
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = MainView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
...

SwiftUI - Half modal?

I'm trying to recreate a Modal just like Safari in iOS13 in SwiftUI:
Here's what it looks like:
Does anyone know if this is possible in SwiftUI? I want to show a small half modal, with the option to drag to fullscreen, just like the sharing sheet.
Any advice is much appreciated!
In Swift 5.5 iOS 15+ and Mac Catalyst 15+ there is a
There is a new solution with adaptiveSheetPresentationController
https://developer.apple.com/documentation/uikit/uipopoverpresentationcontroller/3810055-adaptivesheetpresentationcontrol?changes=__4
#available(iOS 15.0, *)
struct CustomSheetParentView: View {
#State private var isPresented = false
var body: some View {
VStack{
Button("present sheet", action: {
isPresented.toggle()
}).adaptiveSheet(isPresented: $isPresented, detents: [.medium()], smallestUndimmedDetentIdentifier: .large){
Rectangle()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.foregroundColor(.clear)
.border(Color.blue, width: 3)
.overlay(Text("Hello, World!").frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
isPresented.toggle()
}
)
}
}
}
}
#available(iOS 15.0, *)
struct AdaptiveSheet<T: View>: ViewModifier {
let sheetContent: T
#Binding var isPresented: Bool
let detents : [UISheetPresentationController.Detent]
let smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
init(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, #ViewBuilder content: #escaping () -> T) {
self.sheetContent = content()
self.detents = detents
self.smallestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier
self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
self._isPresented = isPresented
}
func body(content: Content) -> some View {
ZStack{
content
CustomSheet_UI(isPresented: $isPresented, detents: detents, smallestUndimmedDetentIdentifier: smallestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, content: {sheetContent}).frame(width: 0, height: 0)
}
}
}
#available(iOS 15.0, *)
extension View {
func adaptiveSheet<T: View>(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, #ViewBuilder content: #escaping () -> T)-> some View {
modifier(AdaptiveSheet(isPresented: isPresented, detents : detents, smallestUndimmedDetentIdentifier: smallestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, content: content))
}
}
#available(iOS 15.0, *)
struct CustomSheet_UI<Content: View>: UIViewControllerRepresentable {
let content: Content
#Binding var isPresented: Bool
let detents : [UISheetPresentationController.Detent]
let smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
init(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, #ViewBuilder content: #escaping () -> Content) {
self.content = content()
self.detents = detents
self.smallestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier
self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
self._isPresented = isPresented
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> CustomSheetViewController<Content> {
let vc = CustomSheetViewController(coordinator: context.coordinator, detents : detents, smallestUndimmedDetentIdentifier: smallestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, content: {content})
return vc
}
func updateUIViewController(_ uiViewController: CustomSheetViewController<Content>, context: Context) {
if isPresented{
uiViewController.presentModalView()
}else{
uiViewController.dismissModalView()
}
}
class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
var parent: CustomSheet_UI
init(_ parent: CustomSheet_UI) {
self.parent = parent
}
//Adjust the variable when the user dismisses with a swipe
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
if parent.isPresented{
parent.isPresented = false
}
}
}
}
#available(iOS 15.0, *)
class CustomSheetViewController<Content: View>: UIViewController {
let content: Content
let coordinator: CustomSheet_UI<Content>.Coordinator
let detents : [UISheetPresentationController.Detent]
let smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
init(coordinator: CustomSheet_UI<Content>.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, #ViewBuilder content: #escaping () -> Content) {
self.content = content()
self.coordinator = coordinator
self.detents = detents
self.smallestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier
self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
super.init(nibName: nil, bundle: .main)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dismissModalView(){
dismiss(animated: true, completion: nil)
}
func presentModalView(){
let hostingController = UIHostingController(rootView: content)
hostingController.modalPresentationStyle = .popover
hostingController.presentationController?.delegate = coordinator as UIAdaptivePresentationControllerDelegate
hostingController.modalTransitionStyle = .coverVertical
if let hostPopover = hostingController.popoverPresentationController {
hostPopover.sourceView = super.view
let sheet = hostPopover.adaptiveSheetPresentationController
//As of 13 Beta 4 if .medium() is the only detent in landscape error occurs
sheet.detents = (isLandscape ? [.large()] : detents)
sheet.largestUndimmedDetentIdentifier =
smallestUndimmedDetentIdentifier
sheet.prefersScrollingExpandsWhenScrolledToEdge =
prefersScrollingExpandsWhenScrolledToEdge
sheet.prefersEdgeAttachedInCompactHeight =
prefersEdgeAttachedInCompactHeight
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
if presentedViewController == nil{
present(hostingController, animated: true, completion: nil)
}
}
/// To compensate for orientation as of 13 Beta 4 only [.large()] works for landscape
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
isLandscape = true
self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = [.large()]
} else {
isLandscape = false
self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = detents
}
}
}
#available(iOS 15.0, *)
struct CustomSheetView_Previews: PreviewProvider {
static var previews: some View {
CustomSheetParentView()
}
}
iOS 16 Beta
In iOS 16 Beta Apple provides a pure SwiftUI solution for a Half-Modal.
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationDetents:(
[.medium, .large],
selection: $settingsDetent
)
}
You can also add custom detents and specify the percentages
static func custom<D>(D.Type) -> PresentationDetent
//A custom detent with a calculated height.
static func fraction(CGFloat) -> PresentationDetent
//A custom detent with the specified fractional height.
static func height(CGFloat) -> PresentationDetent
//A custom detent with the specified height.
Example:
extension PresentationDetent {
static let bar = Self.fraction(0.2)
}
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationDetents:([.bar])
}
I've written a Swift Package that includes a custom modifier that allows you to use the half modal sheet.
Here is the link: https://github.com/AndreaMiotto/PartialSheet
Feel free to use it or to contribute
iOS 16+
It looks like half sheet is finally supported in iOS 16.
To manage the size of sheet we can use PresentationDetent and specifically presentationDetents(_:selection:)
Here's an example from the documentation:
struct ContentView: View {
#State private var showSettings = false
#State private var settingsDetent = PresentationDetent.medium
var body: some View {
Button("View Settings") {
showSettings = true
}
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationDetents:(
[.medium, .large],
selection: $settingsDetent
)
}
}
}
Note that if you provide more that one detent, people can drag the sheet to resize it.
Here are possible values for PresentationDetent:
large
medium
fraction(CGFloat)
height(CGFloat)
custom<D>(D.Type)
You can make your own and place it inside of a zstack:
https://www.mozzafiller.com/posts/swiftui-slide-over-card-like-maps-stocks
struct SlideOverCard<Content: View> : View {
#GestureState private var dragState = DragState.inactive
#State var position = CardPosition.top
var content: () -> Content
var body: some View {
let drag = DragGesture()
.updating($dragState) { drag, state, transaction in
state = .dragging(translation: drag.translation)
}
.onEnded(onDragEnded)
return Group {
Handle()
self.content()
}
.frame(height: UIScreen.main.bounds.height)
.background(Color.white)
.cornerRadius(10.0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: self.position.rawValue + self.dragState.translation.height)
.animation(self.dragState.isDragging ? nil : .spring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.gesture(drag)
}
private func onDragEnded(drag: DragGesture.Value) {
let verticalDirection = drag.predictedEndLocation.y - drag.location.y
let cardTopEdgeLocation = self.position.rawValue + drag.translation.height
let positionAbove: CardPosition
let positionBelow: CardPosition
let closestPosition: CardPosition
if cardTopEdgeLocation <= CardPosition.middle.rawValue {
positionAbove = .top
positionBelow = .middle
} else {
positionAbove = .middle
positionBelow = .bottom
}
if (cardTopEdgeLocation - positionAbove.rawValue) < (positionBelow.rawValue - cardTopEdgeLocation) {
closestPosition = positionAbove
} else {
closestPosition = positionBelow
}
if verticalDirection > 0 {
self.position = positionBelow
} else if verticalDirection < 0 {
self.position = positionAbove
} else {
self.position = closestPosition
}
}
}
enum CardPosition: CGFloat {
case top = 100
case middle = 500
case bottom = 850
}
enum DragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
Here's my naive bottom sheet which scales to its content. Without dragging but it should be relatively easy to add if needed :)
struct BottomSheet<SheetContent: View>: ViewModifier {
#Binding var isPresented: Bool
let sheetContent: () -> SheetContent
func body(content: Content) -> some View {
ZStack {
content
if isPresented {
VStack {
Spacer()
VStack {
HStack {
Spacer()
Button(action: {
withAnimation(.easeInOut) {
self.isPresented = false
}
}) {
Text("done")
.padding(.top, 5)
}
}
sheetContent()
}
.padding()
}
.zIndex(.infinity)
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.bottom)
}
}
}
}
extension View {
func customBottomSheet<SheetContent: View>(
isPresented: Binding<Bool>,
sheetContent: #escaping () -> SheetContent
) -> some View {
self.modifier(BottomSheet(isPresented: isPresented, sheetContent: sheetContent))
}
}
and use like below:
.customBottomSheet(isPresented: $isPickerPresented) {
DatePicker(
"time",
selection: self.$time,
displayedComponents: .hourAndMinute
)
.labelsHidden()
}
As of Beta 2 Beta 3 you can't present a modal View as .fullScreen. It presents as .automatic -> .pageSheet. Even once that's fixed, though, I highly doubt they will give you the drag capability there for free. It would be included in the docs already.
You can use this answer to present full screen for now. Gist here.
Then, after presentation, this is a quick and dirty example of how you can recreate that interaction.
#State var drag: CGFloat = 0.0
var body: some View {
ZStack(alignment: .bottom) {
Spacer() // Use the full space
Color.red
.frame(maxHeight: 300 + self.drag) // Whatever minimum height you want, plus the drag offset
.gesture(
DragGesture(coordinateSpace: .global) // if you use .local the frame will jump around
.onChanged({ (value) in
self.drag = max(0, -value.translation.height)
})
)
}
}
I have written a SwiftUI package which includes custom iOS 13 like half modal and its buttons.
GitHub repo: https://github.com/ViktorMaric/HalfModal
I think almost every iOS developer who writes anything in SwiftUI must come up against this. I certainly did, but I thought that most of the answers here were either too complex or didn't really provide what I wanted.
I've written a very simple partial sheet which is on GitHub, available as a Swift package - HalfASheet
It probably doesn't have the bells & whistles of some of the other solutions, but it does what it needs to do. Plus, writing your own is always good for understanding what's going on.
Note - A couple of things - First of all, this is very much a work-in-progress, please feel free to improve it, etc. Secondly, I've deliberately not done a .podspec as if you're developing for SwiftUI you're on iOS 13 minimum, and the Swift Packages are so much nicer in my opinion...
Andre Carrera's answer is great and feel free to use this guide he provided: https://www.mozzafiller.com/posts/swiftui-slide-over-card-like-maps-stocks
I have modified the SlideOverCard structure so it uses actual device height to measure where the card is supposed to stop (you can play with bounds.height to adjust for your needs):
struct SlideOverCard<Content: View>: View {
var bounds = UIScreen.main.bounds
#GestureState private var dragState = DragState.inactive
#State var position = UIScreen.main.bounds.height/2
var content: () -> Content
var body: some View {
let drag = DragGesture()
.updating($dragState) { drag, state, transaction in
state = .dragging(translation: drag.translation)
}
.onEnded(onDragEnded)
return Group {
Handle()
self.content()
}
.frame(height: UIScreen.main.bounds.height)
.background(Color.white)
.cornerRadius(10.0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: self.position + self.dragState.translation.height)
.animation(self.dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.gesture(drag)
}
private func onDragEnded(drag: DragGesture.Value) {
let verticalDirection = drag.predictedEndLocation.y - drag.location.y
let cardTopEdgeLocation = self.position + drag.translation.height
let positionAbove: CGFloat
let positionBelow: CGFloat
let closestPosition: CGFloat
if cardTopEdgeLocation <= bounds.height/2 {
positionAbove = bounds.height/7
positionBelow = bounds.height/2
} else {
positionAbove = bounds.height/2
positionBelow = bounds.height - (bounds.height/9)
}
if (cardTopEdgeLocation - positionAbove) < (positionBelow - cardTopEdgeLocation) {
closestPosition = positionAbove
} else {
closestPosition = positionBelow
}
if verticalDirection > 0 {
self.position = positionBelow
} else if verticalDirection < 0 {
self.position = positionAbove
} else {
self.position = closestPosition
}
}
}
enum DragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
I was trying to do the same thing asked here, display the share sheet in a natively manner in SwiftUI without to have to implement / import a component.
I've found this solution in https://jeevatamil.medium.com/how-to-create-share-sheet-uiactivityviewcontroller-in-swiftui-cef64b26f073
struct ShareSheetView: View {
var body: some View {
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36)
}
}
func actionSheet() {
guard let data = URL(string: "https://www.zoho.com") else { return }
let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
}
}
>>Update from the WWDC22
You can create half modals or small modals just using this tutorial at the minute 02:40 . It was one of the impressive way to resize the Modal without using any complex code. Just caring about the presentation.
Link video : enter link description here
Let's get from the usage :
.sheet(isPresented : yourbooleanvalue) {
//place some content inside
Text("test")
.presentationDetents([.medium,.large])
}
in this way you set a Modal that can be medium at the start and be dragged up to be large. But you can also use, .small attribute inside of this array of dimensions. I think it was the shortest path and the most use friendly. Now this method saved me life from thousand of lines of code.
In iOS 14, Swift 5, Xcode 12.5 at least, I was able to accomplish this fairly easily by simply wrapping the the UIActivityViewController in another view controller. It doesn't require inspecting the view hierarchy or using any 3rd party libraries. The only hackish part is asynchronously presenting the view controller, which might not even be necessary. Someone with more SwiftUI experience might be able to offer suggestions for improvement.
import Foundation
import SwiftUI
import UIKit
struct ActivityViewController: UIViewControllerRepresentable {
#Binding var shareURL: URL?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let containerViewController = UIViewController()
return containerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
guard let shareURL = shareURL, context.coordinator.presented == false else { return }
context.coordinator.presented = true
let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil)
activityViewController.completionWithItemsHandler = { activity, completed, returnedItems, activityError in
self.shareURL = nil
context.coordinator.presented = false
if completed {
// ...
} else {
// ...
}
}
// Executing this asynchronously might not be necessary but some of my tests
// failed because the view wasn't yet in the view hierarchy on the first pass of updateUIViewController
//
// There might be a better way to test for that condition in the guard statement and execute this
// synchronously if we can be be sure updateUIViewController is invoked at least once after the view is added
DispatchQueue.main.asyncAfter(deadline: .now()) {
uiViewController.present(activityViewController, animated: true)
}
}
class Coordinator: NSObject {
let parent: ActivityViewController
var presented: Bool = false
init(_ parent: ActivityViewController) {
self.parent = parent
}
}
}
struct ContentView: View {
#State var shareURL: URL? = nil
var body: some View {
ZStack {
Button(action: { shareURL = URL(string: "https://apple.com") }) {
Text("Share")
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
if shareURL != nil {
ActivityViewController(shareURL: $shareURL)
}
}
.frame(width: 375, height: 812)
}
}
For a more generic solution, I have come up with the following idea:
https://github.com/mtzaquia/UIKitPresentationModifier
This is a generic modifier that allows you to use UIKit presentations within a SwiftUI view.
From there, the world is your oyster. The only drawback is that you may need to cascade custom environment values from the presenting view into the presented view.
myPresentingView
.presentation(isPresented: $isPresented) {
MyPresentedView()
} controllerProvider: { content in
let controller = UIHostingController(rootView: content)
if #available(iOS 15, *) {
if let sheet = controller.sheetPresentationController {
sheet.preferredCornerRadius = 12
sheet.prefersGrabberVisible = true
}
}
return controller
}
Works by me:
var body: some View {
ZStack {
YOURTOPVIEW()
VStack {
Spacer()
.frame(minWidth: .zero,
maxWidth: .infinity,
minHeight: .zero,
maxHeight: .infinity,
alignment: .top)
YOURBOTTOMVIEW()
.frame(minWidth: .zero,
maxWidth: .infinity,
minHeight: .zero,
maxHeight: .infinity,
alignment: .bottom)
}
}
}