I have an instance of a custom AppKit/UIKit view, wrapped into an **ViewRepresentable that needs to communicate some diagnostic information up the SwiftUI hierarchy. Since this information is not of primary importance, I would like to use the PreferenceKey API to do it, so that parent views can subscribe to it only when needed. Unfortunately, I can't figure a way to set a preference key on my view, since I don't have assess to the view struct itself and thus can't use the preference() API. Is there some way how I can set the preference key from the coordinator?
Here is a simple demo of possible approach. Tested with Xcode 12 / iOS 14. The idea is to construct UIViewRepresentable with injected preference key.
struct DemoView: View {
#State private var diag = CGFloat.zero
var body: some View {
VStack {
Text("Diagnostic >> \(diag)")
.background(MyUIRep.trackingRep)
}
.onPreferenceChange(MyPrefKey.self) { value in
self.diag = value
}
}
}
struct TestPreferenceKeyInRep_Previews: PreviewProvider {
static var previews: some View {
TestPreferenceKeyInRep()
}
}
struct MyUIRep: UIViewRepresentable {
static var trackingRep: some View {
MyUIRep().preference(key: MyPrefKey.self, value: 13.31)
}
func makeUIView(context: Context) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
struct MyPrefKey : PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
Related
Overview: I'm using SwiftUI, but wanted to use UIKit-MapKit. I used UIViewRepresentable to be able to wrap the UIKit feature.
Problem: I'm learning about swiftui-uikit-interoperability and I'm getting stuck on being able to display multiple SwiftUI views.
Code Snippet:
ContentView
struct ContentView: View {
#ObservedObject var viewModel: MapView.PinViewModel
init() {
self.viewModel = MapView.PinViewModel()
}
var body: some View {
NavigationView {
MapView()
.sheet(isPresented: $viewModel.showPinForm) {
PinForm()
}
.navigationTitle("SwiftUI UIKit Interop").scaledToFill()
}
}
}
MapView
struct MapView: UIViewRepresentable {
class PinViewModel: ObservableObject {
#Published var showPinForm: Bool
init() {
self.showPinForm = false
}
func updateShowPinVar() {
self.showPinForm = true
}
}
func showPinForm() {
pinViewModel.updateShowPinVar()
}
func makeCoordinator() -> MapViewCoordinator {
let coordinator = MapViewCoordinator()
coordinator.delegate = self
return coordinator
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 40.7209, longitude: -74.0007)
let span = MKCoordinateSpan(latitudeDelta: 0.03, longitudeDelta: 0.03)
let mapRegion = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(mapRegion, animated: true)
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}
In this I have a #Published var showPinForm that gets toggled in MapView. ContentView is supposed to watch this variable and when it is true it will cause the sheet to pull up. However, I believe when I enter MapView() from ContentView() then I no longer recognize ContentView.
Using the UIViewRepresentable, what is the best way to display another swiftui view? Does not have to use .sheet (Although, it would be nice)
I have tried to simplify the code to show the main problem, so I left out a lot of additional info and took out basic patterns that I used (MVVM)
Please let me know if you need any clarifications
try to follow this pattern, you can toggle the flag both inside and outside your MapView
struct MapView: UIViewRepresentable {
#Binding var switcher: Bool // -> use binding
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct MainView: View {
#ObservedObject var viewModel = MainViewModel()
var body: some View {
MapView(switcher: $viewModel.flag)
.sheet(isPresented: $viewModel.flag) {
Text("Pin pin")
}
}
}
class MainViewModel: ObservableObject {
#Published var flag: Bool = false
}
I have an issue to customize a Mapbox view's style, like for instance adding some information on the map if a switch is switched on. Not sure if it's important, but the layers I need to add are MGLSymbolStyleLayer and MGLLineStyleLayer.
Let's start with code for the main view containing a switch representing a state used to customize the map's style, and an UIViewRepresentable for the Mapbox view.
struct Test_MapBox: View {
#State private var styleURL: URL = MGLStyle.outdoorsStyleURL
#State private var switchButton: Bool = false
var body: some View {
ZStack(alignment: .bottom) {
MapView(switchButton: switchButton)
.styleURL(styleURL)
.edgesIgnoringSafeArea(.all)
Toggle(isOn: $switchButton, label: {
Text("Switch")
})
}
}
}
struct MapView: UIViewRepresentable {
var switchButton: Bool
var mapView = MGLMapView(frame: .zero)
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
print("Style: \(uiView.style)")
print("Update view, switch: \(switchButton)")
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
func styleURL(_ styleURL: URL) -> MapView {
mapView.styleURL = styleURL
return self
}
final class Coordinator: NSObject, MGLMapViewDelegate {
var parent: MapView
init(_ control: MapView) {
self.parent = control
}
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
print("Map loaded, switch: \(parent.switchButton)")
}
func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
print("Style loaded, switch: \(parent.switchButton)")
}
}
}
The issue is the following: inside the delegate functions, switchButton is never up to date, always false (and I don't understand why)... And in updateUIView(), switchButton is OK, but the style is not yet loaded, so usually you get a nil when accessing it ...
Have you got a solution ?
I have the following Class
class GettingData: ObservableObject {
var doneGettingData = false
{
didSet {
if doneGettingData {
print("The instance property doneGettingData is now true.")
} else {
print("The instance property doneGettingData is now false.")
}
}
}
}
And I'm updating the variable doneGettingData within a mapView structure that then I'm using in my main view (contentView).
This variable its changing from false / true while the map gets loaded and I can detecte it from the print that I have used in the class so I know it's changing.
I want to use it to trigger a spinner within ContentView where I have the following code:
import SwiftUI
import Combine
struct ContentView: View {
var done = GettingData().doneGettingData
var body: some View {
VStack {
MapView().edgesIgnoringSafeArea(.top)
Spacer()
Spinner(isAnimating: done, style: .large, color: .red)
}
}
struct Spinner: UIViewRepresentable {
let isAnimating: Bool
let style: UIActivityIndicatorView.Style
let color: UIColor
func makeUIView(context: UIViewRepresentableContext<Spinner>) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(style: style)
spinner.hidesWhenStopped = true
spinner.color = color
return spinner
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<Spinner>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What should I do in order to the variable to change as it is changing with the print but inside the view ? I have tried many options but nothing works !
Thank you
Make your property published
class GettingData: ObservableObject {
#Published var doneGettingData = false
}
then make data observed and pass that instance into MapView for use, so modify that property internally
struct ContentView: View {
#ObservedObject var model = GettingData()
var body: some View {
VStack {
MapView(dataGetter: model).edgesIgnoringSafeArea(.top)
Spacer()
Spinner(isAnimating: model.doneGettingData, style: .large, color: .red)
}
}
In my list, my UIViewRepresentable won't be updated if it is the only item in list. If I add e.g. a Text to it, it works. To see the effect, scroll down and up again.
What am i doing wrong?
Hers is my code:
struct Test : UIViewRepresentable {
var text : String
func makeUIView(context: UIViewRepresentableContext<Test>) -> UILabel {
UILabel()
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Test>) {
uiView.text = text
}
typealias UIViewType = UILabel
}
class Data : ObservableObject {
#Published var names = UIFont.familyNames
}
struct ContentView : View {
#EnvironmentObject var data : Data
var body: some View {
VStack {
List(data.names, id: \.self) { name in
Test(text: name)
// Text(name) // as soon as you comment this out, it works
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
For now I know variant/workaround that works (tested on all available Xcode 11.x)
var body: some View {
VStack {
List(data.names, id: \.self) { name in
Test(text: name).id(name) // << here !!
}
}
}
Note: it might be performance drop on very big lists, but in most usual cases it is not remarkable.
PS: By the way, about class Data - don't name your classes with same names as system one, there might be confuses and unexpectable issues.
I try to run a function in a VStack statement but it don't work. When I run it in a button (with the action label) it work perfectly. How can I insert my func in a VStack?
I declare a QuizData class:
class QuizData: ObservableObject {
var allQuizQuestion: [QuizView] = [QuizView]()
let objectWillChange = PassthroughSubject<QuizData,Never>()
var currentQuestion: Int = 0 {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
and I use it there :
struct Quiz: View {
var continent: Continent
#EnvironmentObject var quizData: QuizData
var body: some View {
VStack
{
generateQuiz(continent: continent, quizData: self.quizData)
quizData.allQuizQuestion[quizData.currentQuestion]
}
.navigationBarTitle (Text(continent.name), displayMode: .inline)
}
}
The func generateQuiz is:
func generateQuiz(continent: Continent, quizData: QuizData) -> Void {
var capital: [Capital]
var alreadyUse: [Int]
for country in CountryData {
if country.continentId == continent.id
{
alreadyUse = [Int]()
capital = [Capital]()
capital.append(CapitalData[country.id])
for _ in 1...3 {
var index = Int.random(in: 1 ... CapitalData.count - 1)
while alreadyUse.contains(index) {
index = Int.random(in: 1 ... CapitalData.count - 1)
}
capital.append(CapitalData[index])
}
capital.shuffle()
quizData.allQuizQuestion.append(QuizView(country: country, question: QuestionData[country.id], capital: capital))
}
}
quizData.allQuizQuestion.shuffle()
}
I need to generate quiz question before the view appear. How should I do this?
First, you can't call a function that doesn't return some View in a VStack closure because that closure is not a normal closure, but a #ViewBuilder closure:
#functionBuilder
struct ViewBuilder {
// Build a value from an empty closure, resulting in an
// empty view in this case:
func buildBlock() -> EmptyView {
return EmptyView()
}
// Build a single view from a closure that contains a single
// view expression:
func buildBlock<V: View>(_ view: V) -> some View {
return view
}
// Build a combining TupleView from a closure that contains
// two view expressions:
func buildBlock<A: View, B: View>(_ viewA: A, viewB: B) -> some View {
return TupleView((viewA, viewB))
}
// And so on, and so forth.
...
}
It's a Swift 5.1 feature that lets you do things like these:
VStack {
Image(uiImage: image)
Text(title)
Text(subtitle)
}
With which you can easily create a view from several other views. For further information take a look at https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api
Now, if I get your issue (correct me if I'm wrong) you need to call a function before your view appears to generate some data. Honestly I'd prefer to pass that data to the view from the outside (creating the data before the view creation). But if you really need it you can do something like:
struct ContentView: View {
private var values: [Int]! = nil
init() {
values = foo()
}
var body: some View {
List(values, id: \.self) { val in
Text("\(val)")
}
}
func foo() -> [Int] {
[0, 1, 2]
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Using the struct init and calling the function at the view creation.
EDIT: To answer your comment here below and since you are using an #EnvironmentObject you can do:
class ContentViewModel: ObservableObject {
#Published var values: [Int]!
init() {
values = generateValues()
}
private func generateValues() -> [Int] {
[0, 1, 2]
}
}
struct ContentView: View {
#EnvironmentObject var contentViewModel: ContentViewModel
var body: some View {
List(contentViewModel.values, id: \.self) { val in
Text("\(val)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ContentViewModel()) //don't forget this
}
}
#endif
And in your SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(ContentViewModel()) //don't forget this
)
self.window = window
window.makeKeyAndVisible()
}
}
This way you are creating a view model for your view and that view model will be accessible throughout your view hierarchy. Every time your view model will change your view will change too.