I found this code for taking a snapshot of a view in SwiftUI, and also found this gist for how to bring up UIActivityController in SwiftUI. It works ok but the biggest issue I am having is when you tap share the UIActivityController is blank, if you tap share again it will work as expected but I can't figure out why it doesn't work the first time? If I change to a static image or text to share it works as expected? Any thoughts?
import SwiftUI
//construct enum to decide which sheet to present:
enum ActiveSheet: String, Identifiable { // <--- note that it's now Identifiable
case photoLibrary, shareSheet
var id: String {
return self.rawValue
}
}
struct ShareHomeView: View {
#State private var shareCardAsImage: UIImage? = nil
#State var activeSheet: ActiveSheet? = nil // <--- now an optional property
var shareCard: some View {
ZStack {
VStack {
Spacer()
LinearGradient(
gradient: Gradient(colors: [.black, .red]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.cornerRadius(10.0)
.padding(.horizontal)
Spacer()
}
SubView()
.padding(.horizontal)
VStack {
HStack {
HStack(alignment: .center) {
Image(systemName: "gamecontroller")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 40)
.padding(.leading)
VStack(alignment: .leading, spacing: 3) {
Text("My App")
.foregroundColor(.white)
.font(.headline)
.fontWeight(.bold)
Text("Wed 30 Mar 22")
.foregroundColor(.white)
.font(.headline)
// .fontWeight(.bold)
}
}
Spacer()
}
.padding([.leading, .top])
Spacer()
}
} //End of ZStack
.frame(height: 350)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
Button {
self.activeSheet = .photoLibrary
} label: {
Image(systemName: "photo")
.resizable()
.scaledToFit()
.frame(height: 40)
}
.padding(.trailing)
}
//GeometryReader { geometry in
shareCard
// } //End of GeometryReader
Button(action: {
shareCardAsImage = shareCard.asImage()
self.activeSheet = .shareSheet
}) {
HStack {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 20))
Text("Share")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50, maxHeight: 50)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(20)
}
.padding(.horizontal)
} //End of Master VStack
//sheet choosing view to display based on selected enum value:
.sheet(item: $activeSheet) { sheet in // <--- sheet is of type ActiveSheet and lets you present the appropriate sheet based on which is active
switch sheet {
case .photoLibrary:
Text("TODO")
case .shareSheet:
if let unwrappedImage = shareCardAsImage {
ShareSheet(photo: unwrappedImage)
}
}
}
//Needed to Wrap in a Navigation View and hide title so that dark mode would work, otherwise this sheet was always in the iPhone's light or dark mode
.navigationBarHidden(true)
.navigationTitle("")
}
}
}
struct RecoveryShareHomeView_Previews: PreviewProvider {
static var previews: some View {
ShareHomeView().preferredColorScheme(.dark)
ShareHomeView().preferredColorScheme(.light)
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
// rendererContext.cgContext.addPath(
// UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
// rendererContext.cgContext.clip()
// As commented by #MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
// DispatchQueue.main.async {
layer.render(in: rendererContext.cgContext)
// }
}
}
}
import LinkPresentation
//This code is from https://gist.github.com/tsuzukihashi/d08fce005a8d892741f4cf965533bd56
struct ShareSheet: UIViewControllerRepresentable {
let photo: UIImage
func makeUIViewController(context: Context) -> UIActivityViewController {
//let text = ""
//let itemSource = ShareActivityItemSource(shareText: text, shareImage: photo)
let activityItems: [Any] = [photo]
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil)
return controller
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
}
}
struct SubView: View {
var body: some View {
HStack {
Image(systemName: "star")
Text("Test View")
Image(systemName: "star")
}
}
}
Add [shareCardAsImage] so that the current value is captured inside sheet:
.sheet(item: $activeSheet) { [shareCardAsImage] sheet in
This is necessary because your item doesn't capture it explicitly, which is generally how item is used. You could also solve it by adding an associated value on your ActiveSheet that stores the image in item.
Related
My goal is to convert SwiftUI view to image. I am currently building it for iOS 15
here is the code that converts the view to image
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
Expected result: https://share.cleanshot.com/5Pvzb7
Actual result: https://share.cleanshot.com/O4GKUF
Below is the body of the view that has a button and image view
var body: some View {
VStack {
imageView
Spacer(minLength: 200)
Button {
UIImageWriteToSavedPhotosAlbum(imageView.snapshot(), nil, nil, nil)
} label: {
Label("Save to Photos", systemImage: "photo")
}
}
}
Imageview is hardcoded with data and image is loaded from the internet
var imageView: some View {
VStack {
ZStack(alignment: .top) {
Color.red
VStack {
VStack {
HStack(alignment: .top) {
Rectangle.init().frame(width: 56, height: 56).cornerRadius(28)
VStack {
HStack {
Text.init("Yura Filin").font(.system(size: 16))
Spacer()
}.padding(.bottom, 1)
HStack {
Text.init("qweqwewqeqqweqeasd asd asd aosidhsa doaid adoaid adiad hiu uh i hiu ih uhuih iuhiu ih asdi ").lineLimit(90).multilineTextAlignment(.leading).font(.system(size: 16))
Spacer()
}.padding(.bottom, 2)
HStack {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: (286 - 64 - 12))
.onReceive(imageLoader.didChange) { data in
self.image = UIImage(data: data) ?? UIImage() }.cornerRadius(14)
Spacer()
}
Spacer()
}.padding(.leading, 12).readSize { size in
print(size)
}
}
}.padding(16).background(.green).cornerRadius(12)
}.padding(64)
}
}.ignoresSafeArea()
}
Any help is appreciated. Thanks!
Unlike when a usual view is rendered and the sizing of that view is constrained by the device, that doesn't happen here. So you need to apply a frame with defined width (and possibly height) to the view you're capturing.
When I needed to do this, I created another view that isn't rendered to the user and used it solely for rendering the image. That way, any sizing you have to define in the frame doesn't mess up what the user sees.
It might take you a while to get it right, but try adding a frame modifier with desired width to the view you're rendering. 😊
Here is the code as shown below which solves your problem and in order to call UIImageWriteToSavedPhotosAlbum() you must add the NSPhotoLibraryAddUsageDescription key to your Info.plist
import SwiftUI
struct ContentView: View {
var imageView: some View {
VStack {
ZStack(alignment: .top) {
Color.red
VStack {
VStack {
HStack(alignment: .top) {
Rectangle.init().frame(width: 56, height: 56).cornerRadius(28)
VStack {
HStack {
Text.init("Yura Filin").font(.system(size: 16))
Spacer()
}.padding(.bottom, 1)
HStack {
Text.init("qweqwewqeqqweqeasd asd asd aosidhsa doaid adoaid adiad hiu uh i hiu ih uhuih iuhiu ih asdi ").lineLimit(90).multilineTextAlignment(.leading).font(.system(size: 16))
Spacer()
}.padding(.bottom, 2)
HStack {
Image("image")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: (286 - 64 - 12))
.cornerRadius(14)
Spacer()
}
Spacer()
}.padding(.leading, 12)
}
}.padding(16).background(.green).cornerRadius(12)
}.padding(64)
}
}.ignoresSafeArea()
}
var body: some View {
VStack {
Button("Save to Photos") {
let image = imageView.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
imageView
Spacer(minLength: 200)
Button("Save to Photos") {
let image = imageView.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
}
}
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Output :
I want the image to not have its own background.
But I don’t understand where to set .background or any other way so that it doesn’t stand out
struct CustomDatePicker: View {
#State var date = Date()
let finalDate = Date.now.addingTimeInterval(604800)
var body: some View {
VStack {
ZStack {
DatePicker("label", selection: $date, in: Date.now...finalDate, displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
Image(systemName: "calendar")
.font(.system(size: 21))
.foregroundColor(titleFieldColor)
.userInteractionDisabled()
}
.background(.clear)
}
}
}
struct NoHitTesting: ViewModifier {
func body(content: Content) -> some View {
SwiftUIWrapper { content }.allowsHitTesting(false)
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
extension View {
func userInteractionDisabled() -> some View {
self.modifier(NoHitTesting())
}
}
And here full string.
var body: some View {
HStack {
Text(title.count > 0 ? title : nameOfCategory.localized(LocalizationService.shared.language))
.frame(width: 200, height: 40, alignment: .leading)
.font(.system(size: 16))
.foregroundColor(titleFieldColor)
CustomDatePicker()
.background(backgroundFieldColor.opacity(1))
.frame(width: 30, height: 30)
}
.frame(width: 280, height: 40)
.background(backgroundFieldColor)
.clipShape(RoundedRectangle(cornerRadius: 25))
}
I have tried many different options but none of the options work for this line:
I can't reduce the frame of this CustomDatePicker
I made .background the same color as view, did oppacity - 1, tried background(.clear)
This white zone doesn't change at all
How else can you try to fix this?
I have created a custom picker that is larger than the native SwiftUI picker. This picker is being used on an iPad which is why I need it larger than usual. When I use the picker, I can't tap on the padding portions. The picker is only opened when I tap directly in the horizontal center of my picker. I have read about using a .frame() modifier to change the tappable area of things like buttons, but that does not seem to work here when I try to add a frame modifier to the base Picker itself. Here is an image of the additional area (in orange) I would like to make tappable
And here is my code:
import SwiftUI
struct CustomPickerStyle: ViewModifier {
var labelText: String
var width: CGFloat
func body(content: Content) -> some View {
Menu {
content
} label: {
HStack {
if let labelText = labelText {
Text(labelText)
.font(.title2)
.fontWeight(.bold)
Spacer()
Image(systemName: "triangle.fill")
.resizable()
.frame(width: 12, height: 8)
.rotationEffect(.degrees(180))
}
}
}
.frame(maxWidth: width, alignment: .leading)
.padding()
.background(.white)
.overlay(
RoundedRectangle(cornerRadius: 3)
.stroke(.gray, lineWidth: 2)
)
}
}
extension View {
func customPickerStyle(labelText: String, width: CGFloat) -> some View {
self.modifier(CustomPickerStyle(labelText: labelText, width: width))
}
}
struct CustomPicker: View {
enum Flavor: String, CaseIterable, Identifiable {
case chocolate, vanilla, strawberry
var id: Self { self }
}
#State private var selectedFlavor: Flavor = .chocolate
var body: some View {
Picker("Flavor", selection: $selectedFlavor) {
Text("Chocolate").tag(Flavor.chocolate)
Text("Vanilla").tag(Flavor.vanilla)
Text("Strawberry").tag(Flavor.strawberry)
}
.customPickerStyle(labelText: selectedFlavor.rawValue, width: 200)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
CustomPicker()
}
}
Just move your padding and background stylings directly inside the label:
struct CustomPickerStyle: ViewModifier {
var labelText: String
var width: CGFloat
func body(content: Content) -> some View {
Menu {
content
} label: {
HStack {
if let labelText = labelText {
Text(labelText)
.font(.title2)
.fontWeight(.bold)
Spacer()
Image(systemName: "triangle.fill")
.resizable()
.frame(width: 12, height: 8)
.rotationEffect(.degrees(180))
}
}
.frame(maxWidth: width, alignment: .leading)
.padding()
.background(.white)
.overlay(
RoundedRectangle(cornerRadius: 3)
.stroke(.gray, lineWidth: 2)
)
}
}
}
I created a banner modifier that displays a banner from the top. This animates well. However, when I tap to dismiss it, it does not animate at all, just hides even though the tap gesture action has withAnimation wrapping it.
struct BannerModifier: ViewModifier {
#Binding var model: BannerData?
func body(content: Content) -> some View {
content.overlay(
Group {
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "exclamationmark.triangle.fill")
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(.red)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.animation(.easeInOut)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
)
}
}
struct BannerData: Identifiable {
let id = UUID()
let title: String
let message: String?
}
In the tap gesture, I wipe out the model but it does not animate. It only hides immediately. How can I animate it so it slide up which is opposite of how it slide down to display? It would be really nice if I can also make the drag gesture interactive so I can slide it out like the native notifications.
Removing view from hierarchy is always animated by container, so to fix your modifier it is needed to apply .animation on some helper container (note: Group is not actually a real container).
Here is corrected variant
struct BannerModifier: ViewModifier {
#Binding var model: BannerData?
func body(content: Content) -> some View {
content.overlay(
VStack { // << holder container !!
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "exclamationmark.triangle.fill")
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.red)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
.animation(.easeInOut) // << here !!
)
}
}
Tested with Xcode 12.1 / iOS 14.1 and test view:
struct TestBannerModifier: View {
#State var model: BannerData?
var body: some View {
VStack {
Button("Test") { model = BannerData(title: "Error", message: "Fix It!")}
Button("Reset") { model = nil }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.modifier(BannerModifier(model: $model))
}
}
This is a slightly improoved version of the banner posted by Asperi
Hope it helps someone.
import SwiftUI
public class BannerData {
public enum BannerType {
case warning, error, success
var textColor: Color {
switch self {
case .warning:
return .black
case .error:
return .white
case .success:
return .white
}
}
var backgroundColor: Color {
switch self {
case .warning:
return .yellow
case .error:
return .red
case .success:
return .green
}
}
var icon: String {
switch self {
case .warning:
return "exclamationmark.triangle.fill"
case .error:
return "exclamationmark.circle.fill"
case .success:
return "checkmark.circle.fill"
}
}
}
var type: BannerType = .success
let title: String
let message: String?
public init(title: String, message: String? = nil, type: BannerType) {
self.title = title
self.message = message
self.type = type
}
}
public struct BannerModifier: ViewModifier {
#Binding var model: BannerData?
public init(model: Binding<BannerData?>) {
_model = model
}
public func body(content: Content) -> some View {
content.overlay(
VStack {
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: model?.type.icon ?? BannerData.BannerType.success.icon)
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
Spacer()
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(model?.type.backgroundColor ?? .clear)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
.animation(.spring())
)
}
}
I'm trying to recreate the iOS 11/12 App Store with SwiftUI.
Let's imagine the "story" is the view displayed when tapping on the card.
I've done the cards, but the problem I'm having now is how to do the animation done to display the "story".
As I'm not good at explaining, here you have a gif:
Gif 1
Gif 2
I've thought of making the whole card a PresentationLink, but the "story" is displayed as a modal, so it doesn't cover the whole screen and doesn't do the animation I want.
The most similar thing would be NavigationLink, but that then obliges me to add a NavigationView, and the card is displayed like another page.
I actually do not care whether its a PresentationLink or NavigationLink or whatever as long as it does the animation and displays the "story".
Thanks in advance.
My code:
Card.swift
struct Card: View {
var icon: UIImage = UIImage(named: "flappy")!
var cardTitle: String = "Welcome to \nCards!"
var cardSubtitle: String = ""
var itemTitle: String = "Flappy Bird"
var itemSubtitle: String = "Flap That!"
var cardCategory: String = ""
var textColor: UIColor = UIColor.white
var background: String = ""
var titleColor: Color = .black
var backgroundColor: Color = .white
var body: some View {
VStack {
if background != "" {
Image(background)
.resizable()
.frame(width: 380, height: 400)
.cornerRadius(20)
} else {
RoundedRectangle(cornerRadius: 20)
.frame(width: 400, height: 400)
.foregroundColor(backgroundColor)
}
VStack {
HStack {
VStack(alignment: .leading) {
if cardCategory != "" {
Text(verbatim: cardCategory.uppercased())
.font(.headline)
.fontWeight(.heavy)
.opacity(0.3)
.foregroundColor(titleColor)
//.opacity(1)
}
HStack {
Text(verbatim: cardTitle)
.font(.largeTitle)
.fontWeight(.heavy)
.lineLimit(3)
.foregroundColor(titleColor)
}
}
Spacer()
}.offset(y: -390)
.padding(.bottom, -390)
HStack {
if cardSubtitle != "" {
Text(verbatim: cardSubtitle)
.font(.system(size: 17))
.foregroundColor(titleColor)
}
Spacer()
}
.offset(y: -50)
.padding(.bottom, -50)
}
.padding(.leading)
}.padding(.leading).padding(.trailing)
}
}
So
Card(cardSubtitle: "Welcome to this library I made :p", cardCategory: "CONNECT", background: "flBackground", titleColor: .white)
displays:
SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.
One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.
In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.
struct ContentView: View {
#State var isPresenting = false
#State var isFullscreen = false
#State var sourceRect: CGRect? = nil
var body: some View {
ZStack {
GeometryReader { proxy in
Button(action: {
self.isFullscreen = false
self.isPresenting = true
self.sourceRect = proxy.frame(in: .global)
}) { ... }
}
if isPresenting {
GeometryReader { proxy in
ModalView()
.frame(
width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil,
height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
.position(
self.isFullscreen ? proxy.frame(in: .global).center :
self.sourceRect?.center ?? proxy.frame(in: .global).center)
.onAppear {
withAnimation {
self.isFullscreen = true
}
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.
Link to Official Documentation
Here's a minimal example:
struct SomeView: View {
#State var isPresented = false
#Namespace var namespace
var body: some View {
VStack {
Button(action: {
withAnimation {
self.isPresented.toggle()
}
}) {
Text("Toggle")
}
SomeSourceContainer {
MatchedView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
}
if isPresented {
SomeTargetContainer {
MatchedTargetView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
}
}
}
}
}