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
}
}
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've got an application that uses UIKeyboardWillShow & Hide notifications in a few different controllers. I've decided to try to consolidate the logic required to move the view with the keyboard into an extension based on a protocol.
Here's my protocol
public protocol KeyboardType : class {
func keyboardWillShow(_ sender: Notification)
func keyboardWillHide(_ sender: Notification)
}
Next, I added an extension to my new protocol so that all I need to do is implement my "KeyboardType protocol and I'll gain access to the necessary functionality to move my view with my keyboard:
Here's my extension
public extension KeyboardType where Self: UIViewController {
func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(Self.keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: self.view.window)
NotificationCenter.default.addObserver(self, selector: #selector(Self.keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: self.view.window)
}
func removeObservers() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: self.view.window)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: self.view.window)
}
func keyboardWillHide(_ sender: Notification) {
let userInfo: [AnyHashable : Any] = (sender as NSNotification).userInfo!
let keyboardSize: CGSize = (userInfo[UIKeyboardFrameBeginUserInfoKey]! as AnyObject).cgRectValue.size
self.view.frame.origin.y += keyboardSize.height
}
func keyboardWillShow(_ sender: Notification) {
let userInfo: [AnyHashable : Any] = sender.userInfo!
let keyboardSize: CGSize = (userInfo[UIKeyboardFrameBeginUserInfoKey]! as AnyObject).cgRectValue.size
let offset: CGSize = (userInfo[UIKeyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
if keyboardSize.height == offset.height {
if self.view.frame.origin.y == 0 {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardSize.height
})
}
} else {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardSize.height - offset.height
})
}
}
}
The problem
The problem is the compiler is asking me to add #objc to my keyboardWillShow and keyboardWillHide methods. When I allow Xcode to add the keywords, the compiler immediately asks me to remove the #objc keywords.
Argument of '#selector' refers to instance method 'keyboardWillShow'
that is not exposed to Objective-C
My Question
How do I expose keyboardWillShow to Objective-C in this situation?
or
Is there a better way to accomplish the same task?
I would take a different approach there extending UIViewController as follow:
protocol KeyboardController {
func keyboardWillShow(_ sender: Notification)
func keyboardWillHide(_ sender: Notification)
}
extension UIViewController: KeyboardController {
func addObservers() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(_:)),
name:.UIKeyboardWillShow,
object: view.window)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide(_:)),
name:.UIKeyboardWillHide,
object: view.window)
}
func removeObservers() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: view.window)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: view.window)
}
func keyboardWillHide(_ notification: Notification) {
print("---> keyboardWillHide")
guard let userInfo = notification.userInfo else { return }
let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
print("keyboardSize", keyboardSize)
}
func keyboardWillShow(_ notification: Notification) {
print("---> keyboardWillShow")
guard
let userInfo = notification.userInfo,
let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size,
let offset = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size
else { return }
if keyboardSize.height == offset.height {
if self.view.frame.origin.y == 0 {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardSize.height
})
}
} else {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardSize.height - offset.height
})
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addObservers()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Another option and the one I like most is to subclass UIViewController:
class KeyboardViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addObservers()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
removeObservers()
}
func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: view.window)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: view.window)
}
func removeObservers() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: view.window)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: view.window)
}
func keyboardWillHide(_ notification: Notification) {
print("---> keyboardWillHide")
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size.height {
print("keyboardHeight", keyboardHeight)
view.frame.origin.y += keyboardHeight
}
}
func keyboardWillShow(_ notification: Notification) {
print("---> keyboardWillShow")
if let userInfo = notification.userInfo,
let keyboardHeight = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size.height,
let offsetHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height {
print("userInfo", userInfo)
print("keyboardHeight", keyboardHeight)
print("offsetHeight", offsetHeight)
if keyboardHeight == offsetHeight {
if self.view.frame.origin.y == 0 {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardHeight
})
}
} else {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardHeight - offsetHeight
})
}
}
}
}
class ViewController: KeyboardViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Have you tried this?:
#objc public protocol KeyboardType {
func keyboardWillShow(_ sender: Notification)
func keyboardWillHide(_ sender: Notification)
}
You also need to import UIKit
I'm creating an Instagram like app, and whenever I dismiss the display alert that is displayed when an image or text is missing, it logs me out and logs me back in. How can I make it so that when I click "ok", only the display alert goes away and doesn't log me out?
Here is my image posting view control code:
import UIKit
import Parse
import Foundation
class PostViewControllerPage1: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func displayAlert(title:String, error:String) {
var alert = UIAlertController(title: title, message: error, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
var photoSelected:Bool = false
#IBOutlet weak var imageToPost: UIImageView!
#IBAction func chooseImage(sender: AnyObject) {
var image = UIImagePickerController()
image.delegate = self
image.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
image.allowsEditing = false
self.presentViewController(image, animated: true, completion: nil)
}
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
println("Image selected")
self.dismissViewControllerAnimated(true, completion: nil)
imageToPost.image = image
photoSelected = true
}
#IBOutlet weak var imageDescription: UITextField!
#IBAction func postImage(sender: AnyObject) {
var error = ""
if (photoSelected == false) {
error = "Please select an image to post"
} else if (imageDescription.text == "") {
error = "Please enter a message"
}
if (error != "") {
displayAlert("Cannot Post Image", error: error)
}
else {
var post = PFObject(className: "Post")
post["Title"] = imageDescription.text
post.saveInBackgroundWithBlock{(success: Bool, error: NSError?) -> Void in
if success == false {
self.displayAlert("Could Not Post Image", error: "Please try again later")
} else {
let imageData = UIImagePNGRepresentation(self.imageToPost.image)
let imageFile = PFFile(name: "image.png", data: imageData)
post["imageFile"] = imageFile
post.saveInBackgroundWithBlock{(success: Bool, error: NSError?) -> Void in
if success == false {
self.displayAlert("Could Not Post Image", error: "Please try again later")
} else {
println("posted successfully")
}
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
What am I doing wrong?
Here is my login code, because I feel like that might be a part of the issue:
import Foundation
import Parse
import UIKit
import Bolts
class LoginViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var loginStatusLabel: UILabel!
#IBOutlet weak var emailTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
emailTextField.delegate = self
passwordTextField.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func loginButtonPress(sender: AnyObject) {
login(emailTextField.text, password: passwordTextField.text)
}
func login(email: String, password: String)
{
PFUser.logInWithUsernameInBackground(email, password: password)
{
(user: PFUser?, error: NSError?) -> Void in
if user != nil
{
user!.fetchInBackground()
if user!.objectForKey("emailVerified") as! Bool
{
self.loginStatusLabel.text = "Success!"
println("successfulLogin")
self.performSegueWithIdentifier("login", sender: self)
}
else if !(user!.objectForKey("emailVerified") as! Bool)
{
self.loginStatusLabel.text = "Verify your email address!"
}
else // status is "missing"
{
//TODO: Handle this error better
self.loginStatusLabel.text = "Verification status: Missing"
}
}
else
{
if let errorField = error!.userInfo
{
self.loginStatusLabel.text = (errorField["error"] as! NSString) as String
}
else
{
// No userInfo dictionary present
// Help from http://stackoverflow.com/questions/25381338/nsobject-anyobject-does-not-have-a-member-named-subscript-error-in-xcode
}
}
}
}
override func viewDidAppear(animated: Bool) {
if PFUser.currentUser() != nil {
self.performSegueWithIdentifier("login", sender: self)
}
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
override func viewWillAppear(animated: Bool) {
self.navigationController?.navigationBarHidden = true
}
override func viewWillDisappear(animated: Bool) {
self.navigationController?.navigationBarHidden = false
}
}
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.dismissViewControllerAnimated in this line will dismiss view controller that show alert and not an alert itself. So all you have to do is comment or delete this line of code