Show JSON as TableView Section and TableView row in Swift 4.2 without Decodable? - tableview

I have below JSON response array with key "events"
{
"events": [
{
"name": "event foo",
"date": "2020-05-22",
"time": "7:00",
"am_or_pm": "PM",
"day": "Saturday",
"description": "test "
},
{
"name": "event bar",
"date": "2020-05-22",
"time": "7:00",
"am_or_pm": "PM",
"day": "Saturday",
"description": "test2"
},
{
"name": "event foobar",
"date": "2020-05-24",
"time": "11:00",
"am_or_pm": "PM",
"day": "Saturday",
"description": "test3"
},
{
"name": "event foobar",
"date": "2020-05-24",
"time": "11:00",
"am_or_pm": "PM",
"day": "Saturday",
"description": "test3"
}
]
}
I want to show above JSON response in TableView as :
"date" key should be TableView Section grouped and Rows as "name" key
Could some help me how will i do that thanks in advance.

First things first, you'll need to create a Model, so that you can transform your JSON into usable objects. To do that, good practice is to use the Codable protocol, which will automatically map your JSON keys to a struct/class variable
struct Event: Codable {
var name: String!
var date: String!
var time: String!
var am_or_pm: String!
var day: String!
var description: String!
}
struct Events: Codable {
var events: [Event]!
}
Now that you have the model that will be generated from the JSON, you need to decode your JSON into your model. There are plenty of ways to do it, I like to use JSONEncoder/JSONDecoder.
So, let's say your JSON string is stored in the variable myJsonString, you would need to
if let jsonData = myJsonString.data(using: .utf8) {
do {
let events = try JSONDecoder().decode(Events.self, from: jsonData)
} catch {
print("Error decoding json!", error.localizedDescription)
}
} else {
print("Failed to get bytes from string!")
}
We can, now turn that code block into a function that returns Events, or nil, if it fails to decode the string.
func getEvents(from jsonString: String) -> Events? {
guard let jsonData = myJsonString.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(Events.self, from: jsonData)
}
Cool! We can easily get our events based on a JSON string now! All we gotta do next is populate the tableView!
To do that, we can create a few functions in our Events struct that will return just what we need to populate our section. The first function will return the sections for our tableView, and the second will return all items for a given section. Let's modify the Events struct
struct Events: Codable {
var events: [Event]!
func getSections() -> [String] {
return Array(Set(self.events.map { $0.date }))
}
func getItems(forSection dateSection: String) -> [Section] {
return self.events.filter {$0.date == dateSection}
}
}
Now, in your TableView datasource class, you need to use the model we created. I`ll do an example for you
class YourTableView: UIViewController, UITableViewDataSource, UITableViewDelegate {
// This is loaded from the getEvents function!
var events: Events!
func numberOfSections (in tableView: UITableView) -> Int {
return self.events.getSections().count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let dateSection = self.events.getSections()[section]
return self.events.getItems(forSection: dateSection ).count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let dateSection = self.events.getSections()[indexPath.section]
let currentEvent = self.getItems(forSection: dateSection)
// Build your cell using currentEvent
}
}
You are probably getting those JSONs from the web, so you have to handle a "loading" state, where you are returning the JSON from the API. That can easily be done by turning the events var into a optional, setting it when you get your JSON and reloading data from your tableView

I did above solutions using Alamofire 5.0 and below is my complete solutions so that it can help someone:
// Create a Modal Class of name "Event"
class Event {
var name: String?
var date: String?
var time: String?
var amOrPm: String?
var day: String?
var description: String?
init(dic: [String: Any]) {
if let name = dic["name"] as? String {
self.name = name
}
if let date = dic["date"] as? String {
self.date = date
}
if let time = dic["time"] as? String {
self.time = time
}
if let amOrPm = dic["am_or_pm"] as? String {
self.amOrPm = amOrPm
}
if let day = dic["day"] as? String {
self.day = day
}
if let description = dic["description"] as? String {
self.description = description
}
}
}
// Creating a Global Class for URL
import Alamofire
class HttpManager {
struct Constants {
static let baseUrl = "https://my-json-server.typicode.com/JCkshone/jsonTest"
}
func getEvents(handledResponse: #escaping (_ data: [[String: Any]])->()) {
let request = AF.request("\(Constants.baseUrl)/db")
request.responseJSON { (response: AFDataResponse<Any>) in
guard let data = response.value as? [String: Any] else { return }
if let events: [[String: Any]] = data["events"] as? [[String: Any]] {
handledResponse(events)
}
}
}
}
// Finally ViewController.swift
struct SectionEvent {
var sectionName: String
var evenst: [Event]
}
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
let http = HttpManager()
var events: [Event] = []
var tableViewData: [SectionEvent] = []
let cell = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
initTableView()
}
func fetchData() {
http.getEvents { (data: [[String : Any]]) in
data.forEach { item in
self.events.append(Event(dic: item))
}
self.buildData()
}
}
func buildData() {
events.forEach { event in
let validation = validateEventExist(event: event)
if !validation.exist {
tableViewData.append(SectionEvent(sectionName: event.date ?? "", evenst: [event]))
} else {
tableViewData[validation.position].evenst.append(event)
}
}
self.tableView.reloadData()
}
func validateEventExist(event: Event) -> (exist: Bool, position: Int) {
let filterData = tableViewData.filter {$0.sectionName == event.date}
let index = tableViewData.firstIndex { $0.sectionName == event.date}
return (filterData.count > 0, index ?? 0)
}
func initTableView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cell)
tableView.tableHeaderView = UIView()
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
tableViewData[section].sectionName
}
func numberOfSections(in tableView: UITableView) -> Int {
tableViewData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
tableViewData[section].evenst.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
if let name = tableViewData[indexPath.section].evenst[indexPath.row].name, let description = tableViewData[indexPath.section].evenst[indexPath.row].description {
cell.textLabel?.text = "\(name) \n\(description)"
cell.textLabel?.numberOfLines = 0
}
return cell
}
}

Related

NSContactPicker not displaying picker window in SwiftUI on macOS

I have tried to get the NSContactPicker to display a picker window in SwiftUI on macOS. Here is my code. If you click on the button nothing happens. What am I missing?
import SwiftUI
import Contacts
import ContactsUI
let d = MyContactPicker()
class MyContactPicker: NSObject, CNContactPickerDelegate
{
var contactName: String = "No user selected"
func pickContact()
{
let contactPicker = CNContactPicker()
contactPicker.delegate = self
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact)
{
contactName = contact.givenName
}
}
struct ContentView: View
{
#State var contact: CNContact?
var picker = MyContactPicker()
var body: some View
{
VStack
{
Text(picker.contactName)
Button("Select Contact")
{
picker.pickContact()
}
}
}
}
Here's a possible starting point using NSViewRepresentable and an NSView subclass
class NSContactPickerView: NSView, CNContactPickerDelegate {
let didSelectContact: (CNContact) -> Void
init(didSelectContact: #escaping (CNContact) -> Void) {
self.didSelectContact = didSelectContact
super.init(frame: .zero)
Task {
showPicker()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showPicker() {
let picker = CNContactPicker()
picker.delegate = self
picker.showRelative(to: .zero, of: self, preferredEdge: .maxY)
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact) {
didSelectContact(contact)
}
}
struct ContactPicker: NSViewRepresentable {
let didSelectContact: (CNContact) -> Void
func makeNSView(context: Context) -> NSContactPickerView {
NSContactPickerView(didSelectContact: didSelectContact)
}
func updateNSView(_ nsView: NSContactPickerView, context: Context) {
}
}
struct ContentView: View {
#State var contact: CNContact?
#State private var showPicker = false
var body: some View {
VStack {
Text(contact?.givenName ?? "")
Button("Select Contact") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker { contact in
self.contact = contact
}
.frame(width: 1, height: 1)
}
}
}
It works, but it's not very elegant. Maybe someone else can improve on this.

Struggling with rapid api and swiftui

Hi I am struggling with getting football data api from rapidapi to work in swift ui . here is the code below
The errors is get are "self.team = decodedTeams" Cannot find 'self' in scope"
and in my content view i get for " $network.getTeams"
Value of type 'EnvironmentObject.Wrapper' has no dynamic member 'getTeams' using key path from root type 'Network'
I have set out what i have in 2 pages of my swiftui code below
any help would be appreciated, I am really struggling with this one
// Network.swift
// Football Scores
//
//
import Foundation
class Network: ObservableObject {
#Published var teams: [Team] = []
}
func getTeams() {
let headers = [
"X-RapidAPI-Key": "MY API KEY",
"X-RapidAPI-Host": "api-football-v1.p.rapidapi.com"
]`
let request = NSMutableURLRequest(url: NSURL(string: "https://api-football-v1.p.rapidapi.com/v3/standings?season=2022&league=39")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
let httpResponse = response as? HTTPURLResponse
print(httpResponse)
DispatchQueue.main.async {
do {
let decodedTeams = try JSONDecoder().decode([Team].self, from: data)
self.team = decodedTeams
} catch let error {
print("Error decoding: ", error)
}
}
}
})
dataTask.resume()
}
and
//
// Team.swift
// Football Scores
//
//
import Foundation
struct Team: Identifiable, Decodable {
var id: Int
var name: String
var logo: String
var points: String
var goaldif: String
}
and
// Football_ScoresApp.swift
// Football Scores
//
import SwiftUI
import Foundation
#main
struct Football_ScoresApp: App {
var network = Network()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(network)
}
}
}
and
import SwiftUI
import CoreData
struct ContentView: View {
#EnvironmentObject var network: Network
var body: some View {
ScrollView {
Text("All teams")
.font(.title).bold()
}
.onAppear {
network.getTeams()
}
VStack(alignment: .leading) {
ForEach(network.teams) { team in
HStack(alignment:.top) {
Text("\(team.id)")
VStack(alignment: .leading) {
Text(team.name)
.bold()
}
}
.frame(width: 300, alignment: .leading)
.padding()
.background(Color(#colorLiteral(red: 0.6667672396, green: 0.7527905703, blue: 1, alpha: 0.2662717301)))
.cornerRadius(20)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Network())
}
}
To display the teams from your api request, first you need to create a set of struct models that
represent the json data that the server is sending. From the server response, you need to
extract the teams information you want to display. Here is my code
that shows how to do it, works well for me, pay attention to the details:
struct ContentView: View {
#StateObject var network = Network() // <-- for testing
var body: some View {
List {
VStack(alignment: .leading) {
ForEach(network.teams) { team in
HStack(alignment:.top) {
Text("\(team.id)")
Text(team.name).bold()
}
.frame(width: 300, alignment: .leading)
.padding()
.background(Color(#colorLiteral(red: 0.6667672396, green: 0.7527905703, blue: 1, alpha: 0.2662717301)))
.cornerRadius(20)
}
}
}
.onAppear {
network.getTeams()
}
}
}
class Network: ObservableObject {
#Published var teams: [Team] = []
func getTeams() {
let token = "your-key" // <--- here your api key
guard let url = URL(string: "https://api-football-v1.p.rapidapi.com/v3/standings?season=2022&league=39") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("\(token)", forHTTPHeaderField: "X-RapidAPI-Key")
request.setValue("api-football-v1.p.rapidapi.com", forHTTPHeaderField: "X-RapidAPI-Host")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
do {
let results = try JSONDecoder().decode(FootyResponse.self, from: data)
// extract just the teams
for response in results.response {
for stand in response.league.standings {
for league in stand {
self.teams.append(league.team)
}
}
}
} catch {
print(error) // <-- here important
}
}
}.resume()
}
}
// MARK: - FootyResponse
struct FootyResponse: Codable {
let welcomeGet: String
let parameters: Parameters
let errors: [String]
let results: Int
let paging: Paging
let response: [Response]
enum CodingKeys: String, CodingKey {
case welcomeGet = "get"
case parameters, errors, results, paging, response
}
}
// MARK: - Paging
struct Paging: Codable {
let current, total: Int
}
// MARK: - Parameters
struct Parameters: Codable {
let league, season: String
}
// MARK: - Response
struct Response: Codable {
let league: League
}
// MARK: - League
struct League: Codable {
let id: Int
let name: String
let country: String
let logo: String
let flag: String
let season: Int
let standings: [[Standing]]
}
// MARK: - Standing
struct Standing: Codable {
let rank: Int
let team: Team
let points, goalsDiff: Int
let group: String
let form: String
let status: String
let standingDescription: String?
let all, home, away: All
let update: String // <-- Date
enum CodingKeys: String, CodingKey {
case rank, team, points, goalsDiff, group, form, status
case standingDescription = "description"
case all, home, away, update
}
}
// MARK: - All
struct All: Codable {
let played, win, draw, lose: Int
let goals: Goals
}
// MARK: - Goals
struct Goals: Codable {
let goalsFor, against: Int
enum CodingKeys: String, CodingKey {
case goalsFor = "for"
case against
}
}
// MARK: - Team
struct Team: Identifiable, Codable {
let id: Int
let name: String
let logo: String
}
EDIT-1:
to get the points of each team, you need to use the Standing struct. Here is an example code to do that.
struct ContentView: View {
#StateObject var network = Network()
var body: some View {
List {
VStack(alignment: .leading) {
ForEach(network.stand) { stand in // <-- here
HStack(alignment:.top) {
Text(stand.team.name).bold() // <-- here
Text("\(stand.points) points") // <-- here
}
.frame(width: 300, alignment: .leading)
.padding()
.background(Color(#colorLiteral(red: 0.6667672396, green: 0.7527905703, blue: 1, alpha: 0.2662717301)))
.cornerRadius(20)
}
}
}
.onAppear {
network.getTeams()
}
}
}
class Network: ObservableObject {
#Published var teams: [Team] = []
#Published var stand: [Standing] = [] // <-- here
func getTeams() {
let token = "your-key" // <--- here your api key
guard let url = URL(string: "https://api-football-v1.p.rapidapi.com/v3/standings?season=2022&league=39") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("\(token)", forHTTPHeaderField: "X-RapidAPI-Key")
request.setValue("api-football-v1.p.rapidapi.com", forHTTPHeaderField: "X-RapidAPI-Host")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
do {
let results = try JSONDecoder().decode(FootyResponse.self, from: data)
// extract the teams and the standings
results.response.forEach{ response in
response.league.standings.forEach{ stand in
self.stand = stand // <--- here
stand.forEach{ league in
self.teams.append(league.team)
}
}
}
} catch {
print(error) // <-- here important
}
}
}.resume()
}
}
// MARK: - Standing
struct Standing: Identifiable, Codable {
let id = UUID() // <-- here
let rank: Int
let team: Team
let points, goalsDiff: Int
let group: String
let form: String
let status: String
let standingDescription: String?
let all, home, away: All
let update: String // <-- Date
enum CodingKeys: String, CodingKey {
case rank, team, points, goalsDiff, group, form, status
case standingDescription = "description"
case all, home, away, update
}
}

Showing 'UIActivityViewController' in SwiftUI

I want to let the user to be able to share a location but I don't know how to show UIActivityViewController in SwiftUI.
The basic implementation of UIActivityViewController in SwiftUI is
import UIKit
import SwiftUI
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
And here is how to use it.
struct MyView: View {
#State private var isSharePresented: Bool = false
var body: some View {
Button("Share app") {
self.isSharePresented = true
}
.sheet(isPresented: $isSharePresented, onDismiss: {
print("Dismiss")
}, content: {
ActivityViewController(activityItems: [URL(string: "https://www.apple.com")!])
})
}
}
Based on Tikhonov's, the following code added a fix to make sure the activity sheet is dismissed properly (if not subsequently the sheet will not be presented).
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
#Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
self.presentationMode.wrappedValue.dismiss()
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
It's a one time thing currently. .sheet will show it as a sheet, but bringing it up again from the same view will have stale data. Those subsequent shows of the sheet will also not trigger any completion handlers. Basically, makeUIViewController is called only once which is the only way to get the data to share into the UIActivityViewController. updateUIViewController has no way to update the data in your activityItems or reset the controller because those are not visible from an instance of UIActivityViewController.
Note that it doesn't work with UIActivityItemSource or UIActivityItemProvider either. Using those is even worse. The placeholder value doesn't show.
I hacked around some more and decided that maybe the problem with my solution was a sheet that was presenting another sheet, and when one went away then the other stayed.
This indirect way of having a ViewController do the presentation when it appears made it work for me.
class UIActivityViewControllerHost: UIViewController {
var message = ""
var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? = nil
override func viewDidAppear(_ animated: Bool) {
share()
}
func share() {
// set up activity view controller
let textToShare = [ message ]
let activityViewController = UIActivityViewController(activityItems: textToShare, applicationActivities: nil)
activityViewController.completionWithItemsHandler = completionWithItemsHandler
activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash
// present the view controller
self.present(activityViewController, animated: true, completion: nil)
}
}
struct ActivityViewController: UIViewControllerRepresentable {
#Binding var text: String
#Binding var showing: Bool
func makeUIViewController(context: Context) -> UIActivityViewControllerHost {
// Create the host and setup the conditions for destroying it
let result = UIActivityViewControllerHost()
result.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
// To indicate to the hosting view this should be "dismissed"
self.showing = false
}
return result
}
func updateUIViewController(_ uiViewController: UIActivityViewControllerHost, context: Context) {
// Update the text in the hosting controller
uiViewController.message = text
}
}
struct ContentView: View {
#State private var showSheet = false
#State private var message = "a message"
var body: some View {
VStack {
TextField("what to share", text: $message)
Button("Hello World") {
self.showSheet = true
}
if showSheet {
ActivityViewController(text: $message, showing: $showSheet)
.frame(width: 0, height: 0)
}
Spacer()
}
.padding()
}
}
May be its not recommended, but it is really easy and two line of code (was for iPhone) to share text
Button(action: {
let shareActivity = UIActivityViewController(activityItems: ["Text To Share"], applicationActivities: nil)
if let vc = UIApplication.shared.windows.first?.rootViewController{
shareActivity.popoverPresentationController?.sourceView = vc.view
//Setup share activity position on screen on bottom center
shareActivity.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height, width: 0, height: 0)
shareActivity.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.down
vc.present(shareActivity, animated: true, completion: nil)
}
}) {
Text("Share")
}
EDIT: Now works fine on iPad (tested on iPad Pro (9.7 -inch) Simulator)
I want to suggest another implementation that looks more native (half screen height without white gap bottom).
import SwiftUI
struct ActivityView: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> ActivityViewWrapper {
ActivityViewWrapper(activityItems: activityItems, applicationActivities: applicationActivities, isPresented: $isPresented)
}
func updateUIViewController(_ uiViewController: ActivityViewWrapper, context: Context) {
uiViewController.isPresented = $isPresented
uiViewController.updateState()
}
}
class ActivityViewWrapper: UIViewController {
var activityItems: [Any]
var applicationActivities: [UIActivity]?
var isPresented: Binding<Bool>
init(activityItems: [Any], applicationActivities: [UIActivity]? = nil, isPresented: Binding<Bool>) {
self.activityItems = activityItems
self.applicationActivities = applicationActivities
self.isPresented = isPresented
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
updateState()
}
fileprivate func updateState() {
guard parent != nil else {return}
let isActivityPresented = presentedViewController != nil
if isActivityPresented != isPresented.wrappedValue {
if !isActivityPresented {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, _, _) in
self.isPresented.wrappedValue = false
}
present(controller, animated: true, completion: nil)
}
else {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
}
}
struct ActivityViewTest: View {
#State private var isActivityPresented = false
var body: some View {
Button("Preset") {
self.isActivityPresented = true
}.background(ActivityView(activityItems: ["Hello, World"], isPresented: $isActivityPresented))
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityViewTest()
}
}
I got it to work now using
.sheet(isPresented: $isSheet, content: { ActivityViewController() }
.presentation is deprecated
It takes up the whole screen iOS 13 style.
If you need more granular control over the content displayed in the share sheet, you will probably end implementing UIActivityItemSource.
I tried using Mike W.'s code above but it didn't work at first (the delegate functions weren't being called). The fix was changing the initialisation of UIActivityController within makeUIViewController as follows, now passing [context.coordinator] as activityItems:
let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)
Also, I wanted to be able to set the icon, title and subtitle in the share sheet, so I have implemented func activityViewControllerLinkMetadata in the Coordinator class.
The following is the complete expanded version of Mike W.'s answer. Please note you will need to add import LinkPresentation to the code.
ActivityViewController
import SwiftUI
import LinkPresentation
struct ActivityViewController: UIViewControllerRepresentable {
var shareable : ActivityShareable?
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)
controller.modalPresentationStyle = .automatic
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func makeCoordinator() -> ActivityViewController.Coordinator {
Coordinator(self.shareable)
}
class Coordinator : NSObject, UIActivityItemSource {
private let shareable : ActivityShareable?
init(_ shareable: ActivityShareable?) {
self.shareable = shareable
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
guard let share = self.shareable else { return "" }
return share.getPlaceholderItem()
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
guard let share = self.shareable else { return "" }
return share.itemForActivityType(activityType: activityType)
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
guard let share = self.shareable else { return "" }
return share.subjectForActivityType(activityType: activityType)
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
guard let share = self.shareable else { return nil }
let metadata = LPLinkMetadata()
// share sheet preview title
metadata.title = share.shareSheetTitle()
// share sheet preview subtitle
metadata.originalURL = URL(fileURLWithPath: share.shareSheetSubTitle())
// share sheet preview icon
if let image = share.shareSheetIcon() {
let imageProvider = NSItemProvider(object: image)
metadata.imageProvider = imageProvider
metadata.iconrovider = imageProvider
}
return metadata
}
}
}
Protocol ActivityShareable
protocol ActivityShareable {
func getPlaceholderItem() -> Any
func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
func shareSheetTitle() -> String
func shareSheetSubTitle() -> String
func shareSheetIcon() -> UIImage?
}
In my case I am using the share sheet to export text, so I created a struct called ActivityShareableText that conforms to ActivityShareable:
struct ActivityShareableText: ActivityShareable {
let text: String
let title: String
let subTitle: String
let icon: UIImage?
func getPlaceholderItem() -> Any {
return text
}
func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any? {
return text
}
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
return "\(title): \(subTitle)"
}
func shareSheetTitle() -> String {
return title
}
func shareSheetSubTitle() -> String {
return subTitle
}
func shareSheetIcon() -> UIImage? {
return icon
}
}
In my code, I call the share sheet as follows:
ActivityViewController(shareable: ActivityShareableText(
text: myShareText(),
title: myShareTitle(),
subTitle: myShareSubTitle(),
icon: UIImage(named: "myAppLogo")
))
FWIW - Providing a slight improvement to answers that includes an implementation for UIActivityItemSource. Code simplified for brevity, specifically around the default return on itemForActivityType and activityViewControllerPlaceholderItem, they must always return the same type.
ActivityViewController
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var shareable : ActivityShareable?
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.modalPresentationStyle = .automatic
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func makeCoordinator() -> ActivityViewController.Coordinator {
Coordinator(self.shareable)
}
class Coordinator : NSObject, UIActivityItemSource {
private let shareable : ActivityShareable?
init(_ shareable: ActivityShareable?) {
self.shareable = shareable
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
guard let share = self.shareable else { return "" }
return share.getPlaceholderItem()
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
guard let share = self.shareable else { return "" }
return share.itemForActivityType(activityType: activityType)
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
guard let share = self.shareable else { return "" }
return share.subjectForActivityType(activityType: activityType)
}
}
}
ActivityShareable
protocol ActivityShareable {
func getPlaceholderItem() -> Any
func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?
/// Optional
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
}
extension ActivityShareable {
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
return ""
}
}
You could pass in the reference for ActivityViewController or the underlying UIActivityViewController but that feels unnecessary.
You could try porting UIActivityViewController to SwiftUI as follows:
struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]
let applicationActivities: [UIActivity]?
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: activityItems,
applicationActivities: applicationActivities)
}
func updateUIViewController(_ uiViewController: UIActivityViewController,
context: UIViewControllerRepresentableContext<ActivityView>) {
}
}
but the app will crash when you try to display it.
I tried: Modal, Popover and NavigationButton.
To test it:
struct ContentView: View {
var body: some Body {
EmptyView
.presentation(Modal(ActivityView()))
}
}
It doesn't seem to be usable from SwiftUI.
Extending upon #Shimanski Artem solution. I think we can write that code more concise. So I basically embed my ActivityViewController in a blank UIViewController and present it from there. This way we don't get the full 'overlay' sheet and you get the native behaviour. Just like #Shimanski Artem did.
struct UIKitActivityView: UIViewControllerRepresentable {
#Binding var isPresented: Bool
let data: [Any]
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
let activityViewController = UIActivityViewController(
activityItems: data,
applicationActivities: nil
)
if isPresented && uiViewController.presentedViewController == nil {
uiViewController.present(activityViewController, animated: true)
}
activityViewController.completionWithItemsHandler = { (_, _, _, _) in
isPresented = false
}
}
}
Usage
struct ActivityViewTest: View {
#State private var isActivityPresented = false
var body: some View {
Button("Preset") {
self.isActivityPresented = true
}
.background(
UIKitActivityView(
isPresented: $viewModel.showShareSheet,
data: ["String"]
)
)
}
}
Example using SwiftUIX
There is a library called SwiftUIX that already has a wrapper for UIActivityViewController. See quick skeleton of how to present it via .sheet() which should be placed somewhere in the var body: some View {}.
import SwiftUIX
/// ...
#State private var showSocialsInviteShareSheet: Bool = false
// ...
.sheet(isPresented: $showSocialsInviteShareSheet, onDismiss: {
print("Dismiss")
}, content: {
AppActivityView(activityItems: [URL(string: "https://www.apple.com")!])
})
Suggest another way to solve it 🤔
You can create the Empty View Controller to present the sheet
struct ShareSheet: UIViewControllerRepresentable {
// To setup the share sheet
struct Config {
let activityItems: [Any]
var applicationActivities: [UIActivity]?
var excludedActivityTypes: [UIActivity.ActivityType]?
}
// Result object
struct Result {
let error: Error?
let activityType: UIActivity.ActivityType?
let completed: Bool
let returnedItems: [Any]?
}
#Binding var isPresented: Bool
private var handler: ((Result) -> Void)?
private let shareSheet: UIActivityViewController
init(
isPresented: Binding<Bool>,
config: Config,
onEnd: ((Result) -> Void)? = nil
) {
self._isPresented = isPresented
shareSheet = UIActivityViewController(
activityItems: config.activityItems,
applicationActivities: config.applicationActivities
)
shareSheet.excludedActivityTypes = config.excludedActivityTypes
shareSheet.completionWithItemsHandler = { activityType, completed, returnedItems, error in
onEnd?(
.init(
error: error,
activityType: activityType,
completed: completed,
returnedItems: returnedItems
)
)
// Set isPresented to false after complete
isPresented.wrappedValue = false
}
}
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: Context
) {
if isPresented, shareSheet.view.window == nil {
uiViewController.present(shareSheet, animated: true, completion: nil)
} else if !isPresented, shareSheet.view.window != nil {
shareSheet.dismiss(animated: true)
}
}
}
You can also create the operator in the view extension
extension View {
func shareSheet(
isPresented: Binding<Bool>,
config: ShareSheet.Config,
onEnd: ((ShareSheet.Result) -> Void)? = nil
) -> some View {
self.background(
ShareSheet(isPresented: isPresented, config: config, onEnd: onEnd)
)
}
}
Thanks for the helpful answers in this thread.
I tried to solve the stale data problem. The issue from not not implementing updateUIViewController in UIViewControllerRepresentable. SwiftUI calls makeUIViewController only once to create the view controller. The method updateUIViewController is responsible to make changes to view controller based on changes of the SwiftUI view.
As UIActivityViewController does not allow to change activityItems and applicationActivities, I used a wrapper view controller. UIViewControllerRepresentable will update the wrapper and the wrapper will create a new UIActivityViewController as needed to perform the update.
Below my code to implement a "share" button in my application. The code is tested on iOS 13.4 beta, which has fixed several SwiftUI bugs - not sure if it works on earlier releases.
struct Share: View {
var document: ReaderDocument // UIDocument subclass
#State var showShareSheet = false
var body: some View {
Button(action: {
self.document.save(to: self.document.fileURL, for: .forOverwriting) { success in
self.showShareSheet = true
}
}) {
Image(systemName: "square.and.arrow.up")
}.popover(isPresented: $showShareSheet) {
ActivityViewController(activityItems: [ self.document.text, self.document.fileURL,
UIPrintInfo.printInfo(), self.printFormatter ])
.frame(minWidth: 320, minHeight: 500) // necessary for iPad
}
}
var printFormatter: UIPrintFormatter {
let fontNum = Preferences.shared.readerFontSize.value
let fontSize = ReaderController.readerFontSizes[fontNum < ReaderController.readerFontSizes.count ? fontNum : 1]
let printFormatter = UISimpleTextPrintFormatter(text: self.document.text)
printFormatter.startPage = 0
printFormatter.perPageContentInsets = UIEdgeInsets(top: 72, left: 72, bottom: 72, right: 72)
return printFormatter
}
}
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
#Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>)
-> WrappedViewController<UIActivityViewController> {
let controller = WrappedViewController(wrappedController: activityController)
return controller
}
func updateUIViewController(_ uiViewController: WrappedViewController<UIActivityViewController>,
context: UIViewControllerRepresentableContext<ActivityViewController>) {
uiViewController.wrappedController = activityController
}
private var activityController: UIActivityViewController {
let avc = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
avc.completionWithItemsHandler = { (_, _, _, _) in
self.presentationMode.wrappedValue.dismiss()
}
return avc
}
}
class WrappedViewController<Controller: UIViewController>: UIViewController {
var wrappedController: Controller {
didSet {
if (wrappedController != oldValue) {
oldValue.removeFromParent()
oldValue.view.removeFromSuperview()
addChild(wrappedController)
view.addSubview(wrappedController.view)
wrappedController.view.frame = view.bounds
}
}
}
init(wrappedController: Controller) {
self.wrappedController = wrappedController
super.init(nibName: nil, bundle: nil)
addChild(wrappedController)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
view.addSubview(wrappedController.view)
wrappedController.view.frame = view.bounds
}
}
Just use introspect. Then you can easily code something like this:
YourView().introspectViewController { controller in
guard let items = viewModel.inviteLinkParams, viewModel.isSharePresented else { return }
let activity = UIActivityViewController(activityItems: items, applicationActivities: nil)
controller.present(activity, animated: true, completion: nil)
}
Swift 5 / SwiftUI Native
Simple, with completion call-back and native SwiftUI #Binding
import SwiftUI
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
#Binding var isPresented: Bool
#Binding var activityItem: String
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback?
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: [activityItem],
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
callback?(activityType, completed, returnedItems, error)
isPresented = false
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
}
}
example usage:
ShareSheet(isPresented: $showShareSheet, activityItem: $sharingUrl, callback: { activityType, completed, returnedItems, error in
print("ShareSheet dismissed: \(activityType) \(completed) \(returnedItems) \(error)")
})

I want to search in TableView using UISearchBar

I have a Big image of video then the user image who uploaded it and video title and description in Table View Cell. using searcher I am searching but only the title changes video image description etc remains that of the first index please help.
Here's the code.
I am bringing data using alamofire in 3 arrays Video_User, Video_Image,
Video_Name and searching on the basis of Video_Name.
#IBOutlet weak var tableview: UITableView!
var Video_User = [String]()
var Video_Image = [String]()
var Video_Name = [String]()
var img = ""
var name = ""
var pass = ""
var searchingvideos = [String]()
var searching = false
override func viewDidLoad()
{
super.viewDidLoad()
getData()
}
func getData()
{
let urlString = "\(AppDelegate.url)get_vid"
Alamofire.request(urlString, method: .post, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseJSON
{
response in
print(response.request as Any)
print(response.response as Any)
print(response.data as Any)
print(response.result as Any)
switch response.result
{
case .success:
if let JSON = response.result.value
{
let dlc = (JSON as! NSDictionary)
print(dlc)
let outPut = dlc.value(forKey: "get_vidResult") as! NSArray
for item in outPut
{
let tempitem = item as! NSDictionary
print(tempitem)
self.Video_Name.append(tempitem.value(forKey: "VideoName") as! String)
self.Video_User.append(tempitem.value(forKey: "UserName") as! String)
self.Video_Image.append(tempitem.value(forKey: "VideoImage") as! String)
}
self.tableview.reloadData()
}
case .failure(let error):
print(error)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching
{
return searchingvideos.count
}
else
{
return Video_Name.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell:VideoCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! VideoCell
cell.V_Header.text = Video_Name[indexPath.row]
cell.V_Footer.text = Video_User[indexPath.row]
cell.V_Image.sd_setImage(with: URL(string: "\(AppDelegate.img_url)\(Video_User[indexPath.row]).jpg"), placeholderImage: #imageLiteral(resourceName: "v"))
cell.U_Image.sd_setImage(with: URL(string: "\(AppDelegate.img_url)\(Video_Image[indexPath.row]).jpg"), placeholderImage: #imageLiteral(resourceName: "v"))
if searching
{
cell.V_Header.text = searchingvideos[indexPath.row]
}
else
{
cell.V_Header.text = Video_Name[indexPath.row]
}
return cell
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
{
searchingvideos = Video_Name.filter({$0.uppercased().prefix(searchText.count) == searchText.uppercased() })
searching = true
tableview.reloadData()
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = false
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.text = ""
}

How to populate TableViewCell with JSON data using Codable protocol in Swift4?

Solved
I am trying to populate Table View Cell from sample JSON in this link https://api.myjson.com/bins/13axs1, but getting error in cellForRowAt function when trying to assign textLabel.
type '[City]?' has no subscript members
JSON
[
{
"name": "New York",
"information": "Later will be added",
"imageUrl": "later"
},
{
"name": "New York",
"information": "Later will be added",
"imageUrl": "later"
}
]
The code in ViewController:
struct City: Codable {
let name: String?
let information: String?
let imageUrl: String?
}
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var citiesList: [City] = []
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://api.myjson.com/bins/13axs1"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let cities = try JSONDecoder().decode([City].self, from: data)
if cities != nil{
self.citiesList = cities
}else {
print("nothing to display")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return citiesList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = citiesList[indexPath.row].name // here I am getting error type '[City]? has no subscript members
return cell
}
}
Sorry, but I don't know why not all of the code shows clearly after I added it, but hope it will be clear for you guys to understand, thanks a lot.
You are missing the reload method (reloadData()):
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let cities = try JSONDecoder().decode([City].self, from: data)
self.citiesList = cities
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print("Error serializing json:", error)
}
}.resume()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.deque...
cell.textLabel?.text = citiesList?[indexPath.row].name
return cell
}

Resources