I have a forEach Array of articles that opens up a detailview for users to read the articles.I have a button to share the article to other apps using UIActivityViewController, mainly will like to share to Wechat though. My problem is to get UIActivityViewController to share a full article, for now i can share only the image like below with a button located inside my detail view:
Button(action: actionSheet) {
Image(systemName: "arrowshape.turn.up.forward.fill")
.font(.headline)
.padding(8)
.background(Color.white)
.cornerRadius(5)
}
The function that pops up the actionsheet:
func actionSheet() {
let activityController = UIActivityViewController(activityItems: [recomendedViewModel.recommend.image], applicationActivities: nil)
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
if UIDevice.current.userInterfaceIdiom == .unspecified{
activityController.popoverPresentationController?.sourceView = window
activityController.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 3, y: UIScreen.main.bounds.height / 1.5, width: 400, height: 400)
}
window?.rootViewController!.present(activityController, animated: true)
}
My ObservedObject for my detail view is :
#ObservedObject var recomendedViewModel: RecommendedEventsRowViewModel
init(recommend: RecommendedEvents) {
self.recomendedViewModel = RecommendedEventsRowViewModel(recommend: recommend)
}
My Array of articles:
struct RecommendedEventsMainView: View {
#ObservedObject var recomendedVM = RecommendedEventsViewModel()
var body: some View {
VStack(spacing: 15) {
ForEach(recomendedVM.recommended) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommend: recommended)) {
NewUpdatedEventsItemView(recommend: recommended)
}
}
}
}
}
I tried putting the ObservedObject from my Array View ( #ObservedObject var recomendedVM = RecommendedEventsViewModel() ) in my detailview and passing it to the actionSheet function above like thus:
let activityController = UIActivityViewController(activityItems: [recomendedVM.recommended], applicationActivities: nil)
It comes blank with no data. Any help is welcome. Thanks in advance.
Related
I have been looking for a short, reusable piece of code that allows to zoom and drag any view in SwiftUI, and also to change the scale independently.
This would be the answer.
The interesting part that I add is that the scale of the zoomed View can be controled from outside via a binding property. So we don't need to depend just on the pinching gesture, but can add a double tap to get the maximum scale, return to the normal scale, or have a slider (for instance) that changes the scale as we please.
I owe the bulk of this code to jtbandes in his answer to this question.
Here you have in a single file the code of the Zoomable and Scrollable view and a Test View to show how it works:
`
import SwiftUI
let maxAllowedScale = 4.0
struct TestZoomableScrollView: View {
#State private var scale: CGFloat = 1.0
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
if scale < maxAllowedScale / 2 {
scale = maxAllowedScale
} else {
scale = 1.0
}
}
}
var body: some View {
VStack(alignment: .center) {
Spacer()
ZoomableScrollView(scale: $scale) {
Image("foto_producto")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
}
.frame(width: 300, height: 300)
.border(.black)
.gesture(doubleTapGesture)
Spacer()
Text("Change the scale")
Slider(value: $scale, in: 0.5...maxAllowedScale + 0.5)
.padding(.horizontal)
Spacer()
}
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
#Binding private var scale: CGFloat
init(scale: Binding<CGFloat>, #ViewBuilder content: () -> Content) {
self._scale = scale
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = maxAllowedScale
scrollView.minimumZoomScale = 1
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.bouncesZoom = true
// Create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), scale: $scale)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
uiView.zoomScale = scale
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var scale: CGFloat
init(hostingController: UIHostingController<Content>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
self._scale = scale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
self.scale = scale
}
}
}
`
I think it's the shortest, easiest way to get the desired behaviour. Also, it works perfectly, something that I haven't found in other solutions offered here. For example, the zooming out is smooth and usually it can be jerky if you don't use this approach.
The slider hast that range to show how the minimun and maximum values are respected, in a real app the range would be 1...maxAllowedScale.
As for the double tap, the behaviour can be changed very easily depending pm what you prefer.
I attach video to show everything at once:
I hope this helps anyone who's looking for this feature.
I'm reposting my question of yesterday and now adding a clean code example to demonstrate the problem
I have a MyCustomMapView, embedding a MKMApView and it starts at a fixed location. I have a function called gotoCoordinate, which accepts a coordinate and then navigates the mapview's center to that coordinate.
In the sample code that can be simulated by clicking on the red button labelleing "Click here to change map position".
This all works great. Until....
in the app I'm working on I also need to have a user location so I have a LocationViewModel handling the request. Once you have given request to access your location, click the button no longer moves the center of the map to that new coordinate.
Once you comment the #StateObject var locationViewModel = LocationViewModel() it is working again.
So it seems that once you are using a location manager with a delegate the map no longer moves when changing it's region
Is this a bug or am I doing something wrong?
import SwiftUI
struct ContentView: View {
#StateObject var locationViewModel = LocationViewModel()
var body: some View {
switch locationViewModel.authorizationStatus {
case .notDetermined:
AnyView(RequestLocationView())
.environmentObject(locationViewModel)
case .restricted:
ErrorView(errorText: "Location use is restricted.")
case .denied:
ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
default:
EmptyView()
}
GeometryReader { geometry in
DisplayMapView(size:geometry.size)
}
}
}
import SwiftUI
import CoreLocation
import MapKit
struct MyCustomMapView: UIViewRepresentable {
var map = MKMapView() // << constructor contract !!
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.showsUserLocation = true
map.showsCompass = true
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
map.setRegion(region, animated: true)
return map
}
func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
map.setRegion(region, animated: true)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MyCustomMapView.Coordinator {
return MyCustomMapView.Coordinator(parent1: self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent:MyCustomMapView
init(parent1:MyCustomMapView){
parent = parent1
}
}//class Coordinator
}
import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
#Environment(\.presentationMode) var presentationMode
var size: CGSize
var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
var map = MyCustomMapView()
var body: some View {
ZStack(alignment:.top){
map
VStack(alignment:.leading){
HStack {
HStack {
Text("Click here to change map position")
.onTapGesture(){
map.gotoCoordinate(startCoordinate)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.black)
.background(Color(.red))
.cornerRadius(10.0)
}
}.padding(.top,50).padding(.leading,20).padding(.trailing,20)
}.ignoresSafeArea()
}
}
import Foundation
import SwiftUI
import CoreLocation
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var authorizationStatus: CLAuthorizationStatus
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
private let locationManager: CLLocationManager
static let shared = LocationViewModel()
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 0.4
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastSeenLocation = locations.first
}
}
struct RequestLocationView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
var body: some View {
VStack(spacing:50) {
Image(systemName: "location.circle")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
Button(action: {
locationViewModel.requestPermission()
}, label: {
Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
})
.padding(10)
.foregroundColor(.white)
.background(.green)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("We need your permission to give you the best experience.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
struct ErrorView: View {
var errorText: String
var body: some View {
VStack {
Image(systemName: "xmark.octagon")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
Text(errorText)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Declare your coordinates as a stateful variable, either as #State or as #Published within an observable object:
struct DisplayMapView: View {
#State var coordinates = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
Then pass the coordinates in as an argument to your view - no need to store your view as a variable:
ZStack(alignment: .top) {
MyMapView(coordinates: coordinates)
VStack(alignment: .leading) {
// etc.
Then you’ll need to do some rejigging in your UIViewRepresentable. You mustn't retain map as a separate instance outside makeUIView and updateUIView - SwiftUI structs can be recreated at will, so that would release your MKMapView instance and create a new one. Instead, the object returned by makeUIView is retained for you by the system. You do need to declare a variable that will accept the coordinates argument above, and then respond to any changes in it in updateUIView.
struct MyMapView: UIViewRepresentable {
var coordinates: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
// etc.
return map
}
func updateUIView(_ uiView: MKMapView, context: Coordinator) {
let region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
uiView.setRegion(region, animated: true)
}
}
Now, when the user taps, instead of calling a function inside your view, you update the DisplayMapView’s coordinates variable and the UIViewRepresentable’s update logic should redraw the map in the correct position.
I am using UIHostingController in one of the apps I'm working on and I have a problem. The embedded SwiftUI View changes its' height dynamically, but I can't seem to get the grasp on how to update it in the view it is embedded in.
The problem doesn't seem to be in the implementation as even the most basic one has this issue.
The UIView is written like this:
var hostingController = UIHostingController(rootView: GrowingView())
override func viewDidLoad() {
super.viewDidLoad()
prepareHostingController()
}
func prepareHostingController() {
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}
and the SwiftUI View is like this:
struct GrowingView: View {
#State var height: CGFloat = 100
var body: some View {
Button(action: tap) {
Rectangle()
.foregroundColor(.red)
.frame(height: height)
}
}
func tap() {
height = 200
}
}
Is there anything obvious I'm missing or is this just the behaviour of the UIHostingController which I can do nothing about? It seems like the latter shouldn't be the case.
So I'm trying to create a custom image picker something like instagram but way more basic. This is how I created the screen using this.
struct NewPostScreen: View {
#StateObject var manager = SelectNewPostScreenManager()
let columns = [GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1)]
var body: some View {
ScrollView {
VStack(spacing: 1) {
Image(uiImage: manager.selectedPhoto?.uiImage ?? UIImage(named: "placeholder-image")!)
.resizable()
.scaledToFit()
.frame(width: 350, height: 350)
.id(1)
LazyVGrid(columns: columns, spacing: 1) {
ForEach(manager.allPhotos) { photo in
Image(uiImage: photo.uiImage)
.resizable()
.scaledToFill()
.frame(maxWidth: UIScreen.main.bounds.width/3, minHeight: UIScreen.main.bounds.width/3, maxHeight: UIScreen.main.bounds.width/3)
.clipped()
.onTapGesture {
manager.selectedPhoto = photo
}
}
}
}
}
}
}
The UI looks good and everything but sometimes when I click an image using the tapGesture it gives me an incorrect selectedPhoto for my manager. Here is how my manager looks and how I fetch the photos from the library.
class SelectNewPostScreenManager: ObservableObject {
#Environment(\.dismiss) var dismiss
#Published var selectedPhoto: Photo?
#Published var allPhotos: [Photo] = []
init() {
fetchPhotos()
}
private func assetsFetchOptions() -> PHFetchOptions {
let fetchOptions = PHFetchOptions()
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
fetchOptions.sortDescriptors = [sortDescriptor]
return fetchOptions
}
func fetchPhotos() {
print("Fetching Photos")
let options = assetsFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .image, options: options)
DispatchQueue.global(qos: . background).async {
allAssets.enumerateObjects { asset, count, _ in
let imageManager = PHImageManager.default()
let targetSize = CGSize(width: 250, height: 250)
let options = PHImageRequestOptions()
options.isSynchronous = true
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, info in
guard let image = image else { return }
let photo = Photo(uiImage: image)
DispatchQueue.main.async {
self.allPhotos.append(photo)
}
}
}
}
}
}
This is how my photo object looks like as well.
struct Photo: Identifiable {
let id = UUID()
let uiImage: UIImage
}
I have no clue to why the tap gesture is not selecting the right item. Ive spent a couple of hours trying to figure out to why this is happening. I might just end up using the UIImagePickerController instead lol.
Anyways if someone can copy and paste this code into a new project of Xcode and run it on your actual device instead of the simulator. Let me know if its happening to you as well.
I was running it on an iPhone X.
The problem is that the image gesture are extending beyond your defined frame, I am sure there are many ways to fix this, but I solved it by adding the contentShape modifier
Please replace your image code with the following
Image(uiImage: photo.uiImage)
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width/3, height: UIScreen.main.bounds.width/3)
.clipped()
.contentShape(Path(CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width/3, height: UIScreen.main.bounds.width/3)))
.onTapGesture {
manager.selectedPhoto = photo
}
contentShape define the hit area for the gesture
SwiftUI has implicit animations with .animate(), and explicit ones using .withAnimation(). However, I can't figure out how to animate an image change:
struct ImageViewWidget : View {
#ObjectBinding var imageLoader: ImageLoader
init(imageURL: URL) {
imageLoader = ImageLoader(imageURL: imageURL)
}
var body: some View {
Image(uiImage:
(imageLoader.data.count == 0) ? UIImage(named: "logo-old")! : UIImage(data: imageLoader.data)!)
.resizable()
.cornerRadius(5)
.frame(width: 120, height:120)
}
}
This Image's uiImage argument is passed the old-logo (placeholder) if there's no data in imageLoader (a BindableObject), and replaces it with the correct one once that's asynchronously loaded:
class ImageLoader : BindableObject {
let didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(imageURL: URL) {
print("Image loader being initted!")
let url = imageURL
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}.resume()
}
}
How can I animate this change, the moment where data.count stops being 0, and we have the image? say I want a fade out-in animation..
If you want to use explicit animations based on environment objects (or observable objects), you need to create some state in your view.
You can react to changes to an observable in your view using onReceive, and then modify your state using explicit animation.
struct ImageViewWidget: View {
#ObservedObject var imageLoader: ImageLoader
#State var uiImage: UIImage = UIImage(named: "logo-old")!
init(imageURL: URL) {
imageLoader = ImageLoader(imageURL: imageURL)
}
var body: some View {
Image(uiImage: uiImage)
.resizable()
.cornerRadius(5)
.frame(width: 120, height: 120)
.onReceive(imageLoader.$data) { data in
if data.count != 0 {
withAnimation {
self.uiImage = UIImage(data: data)!
}
}
}
}
}
You don't necessarily have to call .animate() or .withAnimation() because you are simply switching the images, you can use .transition() instead.
Assuming you have already successfully updated your image with your #ObjectBinding(#ObservedObject in Beta5), you can do this:
var body: some View {
if imageLoader.data.count == 0 {
Image(uiImage: UIImage(named: "logo-old")!)
.resizable()
.cornerRadius(5)
.frame(width: 120, height:120)
.transition(.opacity)
.animation(.easeInOut(duration:1))
} else {
Image(uiImage: UIImage(data: imageLoader.data)!)
.resizable()
.cornerRadius(5)
.frame(width: 120, height:120)
.transition(.opacity)
.animation(.easeInOut(duration:1))
}
}
or you can use a custom view modifier if you want to make the transition fancier:
struct ScaleAndFade: ViewModifier {
/// True when the transition is active.
var isEnabled: Bool
// fade the content view while transitioning in and
// out of the container.
func body(content: Content) -> some View {
return content
.scaleEffect(isEnabled ? 0.1 : 1)
.opacity(isEnabled ? 0 : 1)
//any other properties you want to transition
}
}
extension AnyTransition {
static let scaleAndFade = AnyTransition.modifier(
active: ScaleAndFade(isEnabled: true),
identity: ScaleAndFade(isEnabled: false))
}
and then inside your ImageViewWidget, add .transition(.scaleAndFade) to your Image as its view modifier