SwiftUI - Returning an opaque type in a protocol - swift

Currently, I have a protocol Media which has the method displaySummary() -> some View. The problem is, an opaque type cannot be returned in a protocol, as far as I know.
protocol Media {
func displaySummary() -> some View
}
The implementation code looks like the following:
final class Playlist: Media {
func displaySummary() -> some View {
return HStack {
Text("Summary")
.padding(.all)
.background(Color.black)
}
}
And in the ContentView, I have the following:
let media: Media = Playlist()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
media.displaySummary()
}
}
Is there a way to make this work in SwiftUI?

Here is variant using protocol associatedtype, actually SwiftUI native approach, if we see in its auto-generated module. This allows to avoid type-erasure wrappers and use view directly.
Tested with Xcode 11.4 / iOS 13.4
Update: added ViewBulider, re-tested with Xcode 13.4 / iOS 15.5
protocol Media {
associatedtype Summary : View
func displaySummary() -> Self.Summary
}
final class Playlist: Media, Identifiable {
#ViewBuilder
func displaySummary() -> some View {
HStack {
Text("Summary")
.padding(.all)
.background(Color.black)
}
}
}
struct PlaylistView: View {
let playlist = Playlist()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
playlist.displaySummary()
}
}
}

I was able to solve it using a type-erasure.
protocol Media {
func displaySummary() -> AnyView
}
Simple wrap the opaque View in AnyView:
final class Playlist: Media {
func displaySummary() -> AnyView {
return AnyView(HStack {
Text("Summary")
.padding(.all)
.background(Color.black))
})
}
}

Related

How to implement a generic SingleSelectionView in SwiftUI?

I try to implement a generic View, that should render an array of any items, that are identifiable in a ScrollView. In the view I want to use a view model object that conforms to the protocol SingleSelectionManager, that should provide the data (of type Item) and do some other things.
This is my protocol:
protocol SingleSelectionManager : AnyObject {
associatedtype Item : Identifiable
var items : Array<Item> { get }
func isThisSelected(item: Item) -> Bool
func userDidTapOn(item : Item)
}
And this is my generic View, that causes a lot of errors:
struct GenericSingleSelectionView<Item, Content: View> : View {
let selectionManager : any SingleSelectionManager
var rowContent: (Item, Bool) -> Content
var body: some View {
LazyVStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(selectionManager.items) { item in
rowContent(item, selectionManager.isThisSelected(item: item))
.contentShape(Rectangle())
.onTapGesture {
selectionManager.userDidTapOn(item: item)
}
}
}
}
}
}
I know, I could use a SwiftUI List, but I prefer this approach.
I always struggled with generic protocols and associated types, but most of the time I found a solution. Not in this case.
Within the ForEach view, there are several errors (maybe because this itself is a generic view), like:
Cannot convert value of type 'Array' to expected argument type 'Binding'
Member 'userDidTapOn' cannot be used on value of type 'any SingleSelectionManager'; consider using a generic constraint instead
I would like to know:
Is this even realizable? If so, what I am missing?
Is this generally not possible? If so, what are the reasons?
Thanks for any help.
I think the main problem is 'any'. It doesn't work with associatedtype fine. If you want to use Observedobject, it won't work. Just use generics, try this one.
Or you can implement 'AnySingleSelectionManager'manually, and it'll work.
struct GenericSingleSelectionView<Content: View, Manager: SingleSelectionManager>: View {
let selectionManager : Manager
var rowContent: (Manager.Item, Bool) -> Content
var body: some View {
LazyVStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(selectionManager.items) { item in
rowContent(item, selectionManager.isThisSelected(item: item))
.contentShape(Rectangle())
.onTapGesture {
selectionManager.userDidTapOn(item: item)
}
}
}
}
}
}
And it works.
struct ContentView: View {
var body: some View {
VStack {
GenericSingleSelectionView(
selectionManager: SingleSelectionManagerImpl(),
rowContent: { item, flag in
Text(item.name)
}
)
}
}
}
I think the main problem is when using any SingleSelectionManager, selectionManager.items is different type from item in rowContent(item
ForEach(selectionManager.items) { item in
rowContent(item, selectionManager.isThisSelected(item: item))
But if you change any SingleSelectionManager to a generic class like this
let selectionManager : Manager<Item>
and Manager being
class Manager<Item: Identifiable>: SingleSelectionManager {
var items: [Item] {
return []
}
func isThisSelected(item: Item) -> Bool {
return true
}
func userDidTapOn(item: Item) {
// do nothing
}
}
It should work.
I finally came up with this solution. Thanks to #Christian Moler and #where_am_I for the hint to use a generic class instead of a generic protocol. Turns out, the protocol isn't necessary at all.
So this is my solution, that works:
class SingleSelectionManager<Item : Identifiable> : ObservableObject {
var items: Array<Item>
#Published var selectedItem : Item?
init(items: Array<Item>, selectedItem: Item? = nil) {
self.items = items
self.selectedItem = selectedItem
}
func isThisSelected(item: Item) -> Bool {
selectedItem == nil ? false : selectedItem!.id == item.id
}
func userDidTapOn(item: Item) {
selectedItem = isThisSelected(item: item) ? nil : item
}
}
struct GenericSingleSelectionView<Item : Identifiable, Content: View> : View {
#ObservedObject private (set) var selectionManager : SingleSelectionManager<Item>
var rowContent: (Item, Bool) -> Content
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(selectionManager.items) { item in
rowContent(item, selectionManager.isThisSelected(item: item))
.contentShape(Rectangle())
.onTapGesture {
selectionManager.userDidTapOn(item: item)
}
}
}
}
}
}

SwiftUI: Why is onAppear executing twice? [duplicate]

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

SwiftUI list empty state view/modifier

I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article

Core Data and SwiftUI polymorphism issues

Having troubles putting down together SwiftUI and generic types for handling Core Data.
Consider following example:
Parent is abstract. Foo and Bar are children of Parent and they have some custom attributes.
Now what I want to do, is roughly that:
protocol EntityWithView {
associatedtype T: View
func buildView() -> T
}
extension Parent: EntityWithView {
func buildView() -> some View {
fatalError("Re-implement in child")
}
}
extension Foo {
override func buildView() -> some View {
return Text(footribute)
}
}
extension Bar {
override func buildView() -> some View {
return Text(atrribar)
}
}
struct ViewThatUsesCoreDataAsModel: View {
let entities: [Parent]
var body: some View {
ForEach(entities) { entity in
entity.buildView()
}
}
}
I would want to add polymorphic builder to my core data entities that shape data or build views, that confirm to common interface so I can use them without casting/typing.
Problem that compiler throws errors if I try to modify generated Core data entity directly not through extension, and confirming to protocol though extension doesn't allow overriding.
Ok, this is head-breaking (at least for Preview, which gone crazy), but it works in run-time. Tested with Xcode 11.4 / iOS 13.4.
As we need to do all in extension the idea is to use dispatching via Obj-C messaging, the one actually available pass to override implementation under such requirements.
Note: use Simulator or Device
Complete test module
protocol EntityWithView {
associatedtype T: View
var buildView: T { get }
}
extension Parent {
// allows to use Objective-C run-time messaging by complete
// type erasing.
// By convention subclasses
#objc func generateView() -> Any {
AnyView(EmptyView()) // << safe
//fatalError("stub in base") // << alternate
}
}
extension Parent: EntityWithView {
var buildView: some View {
// restory SwiftUI view type from dispatched message
guard let view = self.generateView() as? AnyView else {
fatalError("Dev error - subview must generate AnyView")
}
return view
}
}
extension Foo {
#objc override func generateView() -> Any {
AnyView(Text(footribute ?? ""))
}
}
extension Bar {
#objc override func generateView() -> Any {
AnyView(Text(attribar ?? ""))
}
}
struct ViewThatUsesCoreDataAsModel: View {
let entities: [Parent]
var body: some View {
VStack {
ForEach(entities, id: \.self) { entity in
entity.buildView
}
}
}
}
struct DemoGeneratingViewInCoreDataExtension: View {
#Environment(\.managedObjectContext) var context
var body: some View {
ViewThatUsesCoreDataAsModel(entities: [
Foo(context: context),
Bar(context: context)
])
}
}

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.