I'm building an app (in swift). One of the options should be to allow the user to either select multiple photos and upload them or take a photo and create a post with it.
My idea is based the following hierarchy:
New Photo post button pressed -> User is presented with UICollection view controller with all photos from their library and an option to select multiple photos (I'm reusing a cell to populate those collection view with the images from the photo library) and above that should be the Camera cell (the user needs to click on the button to allow access to the camera in order to take a photo). Here's a representation from the Simulator.
I've written the code that gets the photos from library and add them in an array here (I'll show the code for these below).
The problem is when you initially load the app for the first time and try to post, you're being asked to grant access to the photos. Despite of the user's choice, (either they agree or don't) they collection view isn't update.
What it needs to happen - When the user is asked to grant access to Photos and they click "OK", it should reload the data in the collection view, but it doesn't. And when they click on "Don't allow" it should dismiss the entire View controller. Here's my code
class CVController: UICollectionViewController, UINavigationControllerDelegate, UICollectionViewDelegateFlowLayout {
var photosLibraryArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
grabAllPhotosFromPhoneLibrary()
}
// Grab All Photos from Library
func grabAllPhotosFromPhoneLibrary () {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // if false = last image we took would show first
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// 1. Are there photos in the library
if fetchResult.count > 0 {
// 2. if fetch.count > 0 it means there's at least 1 photo in the library
for i in 0..<fetchResult.count {
// 3. Cycling through the photos
imageManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler:
{ image, error in
// 4. Append each image in the array
self.photosLibraryArray.append(image!)
})
}
} else {
print("You've got no photos")
self.collectionView?.reloadData()
}
I tried calling collectionView.reloadData() in the viewWillApear(), viewDidApear(), nothing worked.
class CVController: UICollectionViewController, UINavigationControllerDelegate, UICollectionViewDelegateFlowLayout {
var photosLibraryArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
checkPhotoLibraryPermission()
}
Add this below mentioned function
func checkPhotoLibraryPermission() {
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized:
//handle authorized status - // call this function here
grabAllPhotosFromPhoneLibrary()
case .denied, .restricted :
//handle denied status
case .notDetermined:
// ask for permissions
PHPhotoLibrary.requestAuthorization() { status in
switch status {
case .authorized:
// call this function here
grabAllPhotosFromPhoneLibrary()
case .denied, .restricted:
// as above
_ = navigationController?.popViewController(animated: true)
case .notDetermined:
// won't happen but still, if you want to handle.
}
}
}
}
You may accordingly make changes in the following function below. As per the need.
// Grab All Photos from Library
func grabAllPhotosFromPhoneLibrary () {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // if false = last image we took would show first
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// 1. Are there photos in the library
if fetchResult.count > 0 {
// 2. if fetch.count > 0 it means there's at least 1 photo in the library
for i in 0..<fetchResult.count {
// 3. Cycling through the photos
imageManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler:
{ image, error in
// 4. Append each image in the array
self.photosLibraryArray.append(image!)
})
}
//Use this reload data here as - you want the data loaded in the array first and then show the data.
self.collectionView?.reloadData()
} else {
print("You've got no photos")
//self.collectionView?.reloadData()
// if you want to hide the view controller when there is no photo. pop the view controller from your navigation controller like this.
_ = navigationController?.popViewController(animated: true)
}
Related
I've made a widget which fetches Codable data and it's working just fine in the simulator ONLY. The widget updates within 30 seconds or less after the data has changed. I've set a 5 minute update limit (I understand it's called far less frequently). It's working actually really great in the simulator without any kind of background data fetches and updates in less time than I set in getTimeline. Then I ran into an issue on a a real test device.
The data won't update anywhere between 2-10+ mins while testing a real device, in the snapshot it's updated and can see the new data changes but not in the widget on springboard. I don't understand why the simulator works just fine but not a real device. The Widget is definitely being updated when the data changes but only in the Simulator so am I suppose to fetch data in the background?
I've come across this Keeping a Widget Up To Date | Apple Developer Documentation. I'm still very new to Swift and SwiftUI so this is a little bit harder for me to grasp. I'm trying to understand the section Update After Background Network Requests Complete to update my Codeable data. My guess is the simulator is different from a real device and I need to fetch data in the background for the must up to date data?
The end goal is to have the widget update as frequently as possible with the most current data. I'm not sure I even need the background data fetch?
My data model for my widget as an example (which is working fine)
class DataModel {
var data: DataClass = DataClass(results: []))
func sessions(_ completion: #escaping (DataClass -> Void) {
guard let url = URL(string: "URL HERE") else { return }
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(DataClass.self, from: data) {
self.data = response
completion(self.data)
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
}
}
}
.resume()
}
}
My getTimeline calling the data model
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let model = DataModel()
var entries: [SimpleEntry] = []
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
let entry = SimpleEntry(date: entryDate, model: model)
entries.append(entry)
model.sessions {_ in
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have this for my background network request
import Foundation
import WidgetKit
class BackgroundManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
var completionHandler: (() -> Void)? = nil
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "widget-bundleID")
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func update() {
let task = urlSession.downloadTask(with: URL(string: "SAME URL FROM DATA MODEL HERE")!)
task.resume()
}
func urlSession(_ session: URLSession ,downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print (location)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
self.completionHandler!()
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
print("Background update")
}
}
Then in my Widget I set .onBackgroundURLSessionEvents(). I never see any background updates or errors in the console. This seems very wrong, the Codable data will never be updated? How do I properly update my data in the background?
struct Some_Widget: Widget {
let kind: String = "Widget"
let backgroundData = BackgroundManager()
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SomeWidget_WidgetEntryView(entry: entry)
}
.configurationDisplayName("Widget")
.description("Example widget.")
.onBackgroundURLSessionEvents { (sessionIdentifier, completion) in
if sessionIdentifier == self.kind {
self.backgroundData.update()
self.backgroundData.completionHandler = completion
print("background update")
}
}
}
}
Hello I want to add ads to a swiftUI grid. The grid contains pictures that I get from a firebase backend and after every couple of pictures I would like to have an ad.
I am quite new to both SwiftUi and working with ads, so I'm not sure how correct my code is, but here is what I got so far.
// Code for the pictures Grid
struct PicturesGrid: View {
private let data: [Item]
var body: some View {
let gridItems = [GridItem(.fixed(UIScreen.screenWidth / 2),
alignment: .leading),
GridItem(.fixed(UIScreen.screenWidth / 2),
alignment: .leading)]
return ScrollView(showsIndicators: false) {
LazyVGrid(columns: gridItems) {
ForEach(0..<self.data.count, id: \.self) { index in
// Using this workaround for the ad to be on the whole width of the screen
// Also, after every six images I am adding and ad
if index != 0, index % 6 == 0 {
AdView()
.frame(width: UIScreen.screenWidth, height: 280)
.padding(.top, 20)
Spacer()
item
.frame(width: UIScreen.screenWidth / 2)
} else {
item
.frame(width: UIScreen.screenWidth / 2)
}
}
}
}
}
// this is for the picture
var item: some View {
NavigationLink(destination: DetailView(viewModel: DetailViewModel(item: itemAtIndexPath))) {
Cell(viewModel: CellViewModel(item: itemAtIndexPath))
}
.buttonStyle(PlainButtonStyle())
}
}
This is the code that I am currently using to load, create and display an ad
// Code for the ad that I am currently using
struct AdView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let adController = AdViewController(self)
return adController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
class AdViewController: UIViewController {
private var adView: AdView
/// The height constraint applied to the ad view, where necessary.
var heightConstraint: NSLayoutConstraint?
/// The ad loader. You must keep a strong reference to the GADAdLoader during the ad loading
/// process.
var adLoader: GADAdLoader!
/// The native ad view that is being presented.
var nativeAdView: GADUnifiedNativeAdView!
/// The ad unit ID.
let adUnitID = "ca-app-pub-3940256099942544/3986624511"
init(_ adView: AdView) {
self.adView = adView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
var nibView: Any?
nibView = Bundle.main.loadNibNamed("ListAdView", owner: nil, options: nil)?.first
guard let nativeAdView = nibView as? GADUnifiedNativeAdView else {
return
}
setAdView(nativeAdView)
adLoader = GADAdLoader(adUnitID: adUnitID, rootViewController: self,
adTypes: [.unifiedNative], options: nil)
adLoader.delegate = self
DispatchQueue.global(qos: .background).async {
self.adLoader.load(GADRequest())
}
}
func setAdView(_ adView: GADUnifiedNativeAdView) {
// Remove the previous ad view.
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else {
return
}
weakSelf.nativeAdView = adView
weakSelf.view.addSubview(weakSelf.nativeAdView)
weakSelf.nativeAdView.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for positioning the native ad view to stretch the entire width and height
let viewDictionary = ["_nativeAdView": weakSelf.nativeAdView!]
weakSelf.view.addConstraints(
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[_nativeAdView]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
)
weakSelf.view.addConstraints(
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[_nativeAdView]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
)
}
}
}
extension AdViewController: GADUnifiedNativeAdLoaderDelegate {
func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error:
GADRequestError) {
print("didFailToReceiveAdWithError: \(error)")
}
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
print("Received unified native ad: \(nativeAd)")
// Deactivate the height constraint that was set when the previous video ad loaded.
heightConstraint?.isActive = false
// Populate the native ad view with the native ad assets.
// The headline and mediaContent are guaranteed to be present in every native ad.
(nativeAdView.headlineView as? UILabel)?.text = nativeAd.headline
nativeAdView.mediaView?.mediaContent = nativeAd.mediaContent
// This app uses a fixed width for the GADMediaView and changes its height to match the aspect
// ratio of the media it displays.
if let mediaView = nativeAdView.mediaView, nativeAd.mediaContent.aspectRatio > 0 {
heightConstraint = NSLayoutConstraint(
item: mediaView,
attribute: .height,
relatedBy: .equal,
toItem: mediaView,
attribute: .width,
multiplier: CGFloat(1 / nativeAd.mediaContent.aspectRatio),
constant: 0)
heightConstraint?.isActive = true
}
// This asset is not guaranteed to be present. Check that it is before
// showing or hiding it.
(nativeAdView.advertiserView as? UILabel)?.text = nativeAd.advertiser
nativeAdView.advertiserView?.isHidden = nativeAd.advertiser == nil
// In order for the SDK to process touch events properly, user interaction should be disabled.
nativeAdView.callToActionView?.isUserInteractionEnabled = false
// Associate the native ad view with the native ad object. This is
// required to make the ad clickable.
// Note: this should always be done after populating the ad views.
nativeAdView.nativeAd = nativeAd
}
}
I want to mention that this is working at the moment, but the problems that I want to fix and I don't know how are:
The grid with the pictures load, but when I scroll over an ad, it takes several seconds for the ad to load and display. How could I at least hide it while it loads or make it faster?
If I scroll over an ad, the ad loads and if I continue scrolling, when I scroll back up, the ad is not loaded anymore and I have to wait for it to load again. How can I fix this? Or what is the best practice for this kind of scenario?
Should I use multipleAds? To load them before showing? If yes, then how should I do this?
Does what I am doing here look even a little bit correct? Please...I need some help
The Best Way to show ads in SwiftUI Grids is implementing Native Ads in your app to provide personalized ad experience
I am working on a login view and trying to change the border color of a UITextField in Xcode/swift3 when validation of the textfield fails. The UITextField should get a red border color.
The problem is that if enter an email, then a password and then press the submit button, i have to focus email text field again before it gets a red border.
This is my LoginViewController.swift so far:
import Foundation
import UIKit
class LoginViewController : UIViewController, UITextFieldDelegate {
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
// login button action
#IBAction func loginButtonTabbed(_ sender: Any) {
// getting values from text fields
let userEmail = userEmailTextField.text;
let userPassword = userPasswordTextField.text;
// set enpoind data
let requestURL = NSURL(string: Constants.apiUrl)
//creating a task to send the post request
var request = URLRequest(url: requestURL as! URL)
request.httpMethod = "POST"
let postString = "cmd=addUser&email="+userEmail!+"&password="+userPassword!
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
do {
let json = try? JSONSerialization.jsonObject(with: data, options: [])
// store json response to dictionary
if let dictionary = json as? [String: Any] {
// check if we got validation errors
if let nestedDictionary = dictionary["validation"] as? [String: Any] {
// display validation messages on device
if let emailMsg = nestedDictionary["Email"] as? String { // change color of textfield
self.userEmailTextField.errorField()
}
}
}
} catch let error as NSError {
print(error)
}
}
//executing the task
task.resume()
}
}
and the UITextField extension UITextField.swift:
import Foundation
import UIKit
extension UITextField {
func errorField(){
self.layer.borderColor = UIColor(red: 255/255.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).cgColor
self.layer.borderWidth = 1.0;
}
}
When you're doing a network call, it always happens in the background...so in order to do any kind of UI updates you need to be on the main queue. Just put the self.userEmailTextField.errorField() inside DispatchQueue.main.async {...} so it would be done immediately.
Also haven't really tested your code very well. Why?
Even in your current code the border would still turn red, but it turns red after almost like 6-7 seconds (it could take less or more for you)...because it's being ran from background thread.
What I don't understand is why clicking on the textField again brings the red border right away!? Here's what I'm guessing happens:
From the background thread you update the model ie change the textField color which queues the UI/view to be updated...but since we're on a background queue, that UI updated could take a few seconds to happen
But then you tapped on the textField right away and forced a super quick read of the textField and all its properties which includes the border—from main thread (actual user touches are always handled through main thread)...which even though are not yet red on the screen, but since it's red on the model it will read from it and change color to red immediately.
I am looping through an array of string arrays. I am comparing the element at index 0 to the title of the button pressed on the page(ideally). But I am getting an Unresolved Identifier error, which means I am doing something wrong. How Can I compare the element in the loop to the title of the button made programatically on the page. Here is my code! The issue is with the function at the bottom, in the if statement that is in the for loop. I don't know how to say 'if this index position of the element is equal too the title of the button pressed'.
import UIKit
import AVKit
import AVFoundation
class ViewController: UIViewController {
var songArray: [Array<String>] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//connect to website
let url = URL(string:"*******")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil
{
print("error")
}
else
{
if let content = data
{
do
{
//download JSON data from php page, display data
let JSON = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as! [[String]]
print(JSON)
//Make buttons with JSON array
var buttonY: CGFloat = 20
for song in JSON {
self.songArray.append(song)
let SongButton = UIButton(frame: CGRect(x: 50, y: buttonY, width: 250, height: 30))
buttonY = buttonY + 50 // 50px spacing
SongButton.layer.cornerRadius = 10 //Edge formatting for buttons
SongButton.backgroundColor = UIColor.darkGray //Color for buttons
SongButton.setTitle("\(song[0])", for: UIControlState.normal) //button title
SongButton.titleLabel?.text = "\(song[0])"
SongButton.addTarget(self,action: #selector(self.songButtonPressed(_:)), for: UIControlEvents.touchUpInside) //button press / response
self.view.addSubview(SongButton) // adds buttons to view
}
}
catch
{
}
}
}
}
task.resume()
print(songArray)
}
func songButtonPressed(_ sender:UIButton!) { // function for buttons
for song in songArray {
if "\(song[0])" == SongButton.titleLabel?.text {
let URL = NSURL(string: "\(song[1])")
let player = AVPlayer(url: URL! as URL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
player.play()
}
}
}
}
My train of thought is to loop through the array of arrays, compare index position 0 to all button titles on the page, and if it matches, plus index position 2 into the AV player. Thanks for any help or advice on the logic behind my code, I am a beginner and I know this level of programming is a bit over my head
It's most likely
for song in songArray {
// the string interpolation "\()" is redundant
if song[0] == sender.titleLabel?.text { ... }
or maybe
if song[0] == sender.title { ... }
SongButton is not related to the action method.
I am developing music app but for lock screen control i am not able to assign time duration and elapsed time
here is my code
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.previousTrackCommand.isEnabled = true;
commandCenter.previousTrackCommand.addTarget(self, action:#selector(home_ViewController.btn_rewind(_:)))
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action:#selector(home_ViewController.btn_fast(_:)))
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget(self, action:#selector(home_ViewController.play_player))
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget(self, action:#selector(home_ViewController.pause_player))
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action:#selector(home_ViewController.togglePlay_Pause))
commandCenter.skipBackwardCommand.isEnabled = false
commandCenter.skipForwardCommand.isEnabled = false
if #available(iOS 9.1, *) {
commandCenter.changePlaybackPositionCommand.isEnabled = true
} else {
// Fallback on earlier versions
return
}
and media info
func setLockInfo()
{
let url = URL(string: song_UrlString)
let data = try? Data(contentsOf: url!)
let art = MPMediaItemArtwork.init(image: UIImage(data: data!)!)
let songInfo :[String : Any] = [MPMediaItemPropertyTitle :st_title,MPMediaItemPropertyArtwork : art]
MPNowPlayingInfoCenter.default().nowPlayingInfo = songInfo
}
I am getting title and image but lock screen is not displaying time
I am using SWIFT 3 to code
It's not displaying the time because you're not telling it to display the time.
To show the playback time, your nowPlayingInfo dictionary needs to include values for the keys:
MPNowPlayingInfoPropertyElapsedPlaybackTime, so it knows what the current time is when you start playing,
MPMediaItemPropertyPlaybackDuration, so it knows what the current time is relative to in the bar, and
MPNowPlayingInfoPropertyPlaybackRate, so it can automatically update the playback time UI without you needing to periodically set the current time.
If you want the playback time bar to be interactive (that is, allow jumping to a different time instead of just displaying the current time, register a changePlaybackPositionCommand with your remote command center.
Swift 3 example with all commands woking, including changePlaybackPositionCommand:
func remotePlayerInit() {
UIApplication.shared.beginReceivingRemoteControlEvents()
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.pauseCommand.addTarget(self, action: #selector(self.pauseSongTouch(_:)))
commandCenter.playCommand.addTarget(self, action: #selector(self.playSongTouch(_:)))
commandCenter.nextTrackCommand.addTarget(self, action: #selector(self.nextSongTouch(_:)))
commandCenter.previousTrackCommand.addTarget(self, action: #selector(self.previousSongTouch(_:)))
commandCenter.changePlaybackPositionCommand.addTarget(self, action: #selector(self.changedThumbSlider(_:)))
setLockInfo()
}
func changedThumbSlider(_ event: MPChangePlaybackPositionCommandEvent) -> MPRemoteCommandHandlerStatus {
audioPlayer.currentTime = event.positionTime
setLockInfo()
return .success
}
func setLockInfo()
{
let image = PlayerVC.songs[PlayerVC.currentSelection].image
let artwork = MPMediaItemArtwork.init(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
let songInfo: [String: Any] = [MPMediaItemPropertyTitle: PlayerVC.songs[PlayerVC.currentSelection].name,
MPMediaItemPropertyArtwork: artwork,
MPNowPlayingInfoPropertyElapsedPlaybackTime: TimeInterval(audioPlayer.currentTime),
MPNowPlayingInfoPropertyPlaybackRate: 1,
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration,
MPMediaItemPropertyArtist: "Artist"]
MPNowPlayingInfoCenter.default().nowPlayingInfo = songInfo
}