I have a WKWebView that is used to present the login screen of my OAuth Identity provider.
import UIKit
import WebKit
protocol OAuth2WKWebViewDelegate: class {
func didReceiveAuthorizationCode(_ code: String) -> Void
func didRevokeSession() -> Void
}
class OAuth2WKWebViewController: UIViewController {
let targetUrl: URLComponents
let webView = WKWebView()
weak var delegate: OAuth2WKWebViewDelegate?
init(targetUrl: URLComponents) {
self.targetUrl = targetUrl
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
loadUrl()
}
}
extension OAuth2WKWebViewController: WKNavigationDelegate {
func loadUrl() {
guard let url = targetUrl.url else { return }
view = webView
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
webView.navigationDelegate = self
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url {
if url.scheme == "appdev", url.absoluteString.range(of: "code") != nil {
let urlParts = url.absoluteString.components(separatedBy: "?")
let code = urlParts[1].components(separatedBy: "code=")[1]
delegate?.didReceiveAuthorizationCode(code)
}
if url.absoluteString == "appdev://oauth-callback-after-sign-out" {
delegate?.didRevokeSession()
}
}
decisionHandler(.allow)
}
}
I also have an IdentityService I use to present this view and respond to it's success / error.
protocol IdentityServiceProtocol {
var hasValidToken: Bool { get }
func initAuthCodeFlow() -> Void
func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void
func storeOAuthTokens(accessToken: String, refreshToken: String, completion: #escaping () -> Void) -> Void
func renderAuthView() -> Void
}
class IdentityService: IdentityServiceProtocol {
fileprivate var apiClient: APIClient
fileprivate var keyChainService: KeyChainService
init(apiClient: APIClient = APIClient(), keyChainService: KeyChainService = KeyChainService()) {
self.apiClient = apiClient
self.keyChainService = keyChainService
}
var hasValidToken: Bool {
return keyChainService.fetchSingleObject(withKey: "AccessToken") != nil
}
func initAuthCodeFlow() -> Void {
let queryItems = ["response_type": "code", "client_id": clientId, "redirect_uri": redirectUri, "state": state, "scope": scope]
renderOAuthWebView(forService: .auth(company: "benefex"), queryitems: queryItems)
}
func initRevokeSession() -> Void {
guard let refreshToken = keyChainService.fetchSingleObject(withKey: "RefreshToken") else { return }
let queryItems = ["refresh_token": refreshToken]
renderOAuthWebView(forService: .revokeSession(company: "benefex"), queryitems: queryItems)
}
func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void {
guard let targetUrl = constructURLComponents(endPoint: service, queryItems: queryitems) else { return }
let webView = OAuth2WKWebViewController(targetUrl: targetUrl)
webView.delegate = self
UIApplication.shared.windows.first?.rootViewController = webView
}
func storeOAuthTokens(accessToken: String, refreshToken: String, completion: #escaping ()-> Void) -> Void {
let success = keyChainService.storeManyObjects(["AccessToken": accessToken, "RefreshToken": refreshToken])
guard success == true else { return }
completion()
}
func renderAuthView() -> Void {
UIApplication.shared.windows.first?.rootViewController = UINavigationController.init(rootViewController: AuthenticatedViewController())
}
}
extension IdentityService: OAuth2WKWebViewDelegate {
func didReceiveAuthorizationCode(_ code: String) {
apiClient.call(endpoint: IdentityEndpoint.accessToken(company: "benefex", code: code)) { [weak self] (response: OAuthTokenResponse) in
switch response {
case .success(let payload):
guard let accessToken = payload.accessToken, let refreshToken = payload.refreshToken else { return }
self?.storeOAuthTokens(accessToken: accessToken, refreshToken: refreshToken) { self?.renderAuthView() }
case .error:
// login failed for some reason
print("could not complete request for access token")
}
}
}
func didRevokeSession() {
print("This was called")
}
}
extension IdentityService {
fileprivate var state: String {
return generateState(withLength: 20)
}
fileprivate func constructURLComponents(endPoint: IdentityEndpoint, queryItems: [String: String]) -> URLComponents? {
var url = URLComponents(url: endPoint.baseUrl, resolvingAgainstBaseURL: false)
url?.path = endPoint.path
url?.queryItems = queryItems.map { URLQueryItem(name: $0.key, value: $0.value) }
return url
}
fileprivate func generateState(withLength len: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let length = UInt32(letters.count)
var randomString = ""
for _ in 0..<len {
let rand = arc4random_uniform(length)
let idx = letters.index(letters.startIndex, offsetBy: Int(rand))
let letter = letters[idx]
randomString += String(letter)
}
return randomString
}
}
extension IdentityService {
var clientId: String {
let envVar = ProcessInfo.processInfo.environment
guard let value = envVar["APP_CLIENT_ID"] else { fatalError("Missing APP_CLIENT_ID enviroment variable") }
return value
}
var redirectUri: String {
let envVar = ProcessInfo.processInfo.environment
guard let value = envVar["APP_REDIRECT_URI"] else { fatalError("Missing APP_REDIRECT_URI enviroment variable") }
return value
}
var scope: String {
let envVar = ProcessInfo.processInfo.environment
guard let value = envVar["APP_SCOPES"] else { fatalError("Missing APP_SCOPES enviroment variable") }
return value
}
}
The delegate?.didReceiveAuthorizationCode(code) is working.
However when delegate?.didRevokeSession() is called from the WebView, the identity service does not respond.
I added some console logs and can see my IdentityService is being de - init when I invoke the logout method.
I believe this is causing it to do nothing when the delegate method fires.
How can I ensure the delegate method is called still?
This came up when I was searching for answers to my issue - if you are using the 10.2 or 10.2.1 compiler - an issue was occurring for us when compiling in Release (instead of Debug) - where the delegate functions are not being called
The fix for us was to include #objc before all delegate function calls, IE
#objc func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
If you are in the same scenario - this should help
Related
Purpose: To log in to Web3.0 using metamask in iOS, and to save login sessions to maintain repeated logins.
Problem: The current login is successful, but I don't know where to handle the session.
Source:
https://github.com/maurovz/Glaip <- I use this API
Among those who have implemented WalletConnect in the Swift environment, if anyone knows how to get a session, I'm desperately asking for your help.
// This is Button to login Metamask. action call a function 'glaip.loginUser'
WalletButtonView(
title: "MetaMask",
action: {
glaip.loginUser(type: .MetaMask) { result in
switch result {
case .success(let user):
print("walletAddress:\(user.wallet.address)")
`walletInfo`.walletAddress = user.wallet.address
`walletInfo`.setNFTInfo(user.wallet.address)
case .failure(let error):
print(error)
}
}
},
iconImage: Image("MetaMaskIcon")
)
// This is loginUser function.
public func loginUser(type: WalletType, completion: #escaping (Result<User, Error>) -> Void) {
walletLogin(wallet: type, completion: { [weak self] result in
switch result {
case let .success(user):
DispatchQueue.main.async {
self?.userState = .loggedIn(user)
}
self?.currentWallet = type
completion(.success(user))
case let .failure(error):
completion(.failure(error))
}
})
}
public protocol WalletService {
func connect(wallet: WalletType, completion: #escaping (Result<WalletDetails, Error>) -> Void)
func sign(wallet: WalletType, message: String, completion: #escaping (Result<String, Error>) -> Void)
}
public struct WalletDetails {
public let address: String
public let chainId: Int
public init(address: String, chainId: Int) {
self.address = address
self.chainId = chainId
}
}
public final class WalletLinkService: WalletService {
private let title: String
private let description: String
private var walletConnect: WalletConnect!
private var onDidConnect: ((WalletDetails) -> Void)?
public init(title: String, description: String) {
self.title = title
self.description = description
setWalletConnect()
}
public func connect(wallet: WalletType, completion: #escaping (Result<WalletDetails, Error>) -> Void) {
openAppToConnect(wallet: wallet, getDeepLink(wallet: wallet), delay: 1)
// Temp fix to avoid threading issue with async await
let lock = NSLock()
onDidConnect = { walletInfo in
lock.lock()
defer { lock.unlock() }
completion(.success(walletInfo))
}
}
public func disconnect() {
do {
guard let session = walletConnect.session else { return }
try walletConnect.client.disconnect(from: session)
} catch {
print("error disconnecting")
}
}
public func sign(wallet: WalletType, message: String, completion: #escaping (Result<String, Error>) -> Void) {
openAppToConnect(wallet: wallet, getDeepLink(wallet: .MetaMask), delay: 3)
walletConnect.sign(message: message, completion: completion)
}
private func setWalletConnect() {
walletConnect = WalletConnect(delegate: self)
walletConnect.reconnectIfNeeded()
}
private func openAppToConnect(wallet: WalletType, _ url: String, delay: CGFloat = 0) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if let url = URL(string: self.getDeepLink(wallet: wallet)), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
private func getDeepLink(wallet: WalletType) -> String {
let connectionUrl = walletConnect.connect(title: title, description: description)
let encodeURL = connectionUrl.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
let end = encodeURL.replacingOccurrences(of: "=", with: "%3D").replacingOccurrences(of: "&", with: "%26")
return "\(wallet.rawValue)\(end)"
}
}
// MARK: - WalletConnectDelegate
extension WalletLinkService: WalletConnectDelegate {
func didUpdate() {
}
func failedToConnect() {
}
func didConnect() {
guard
let session = walletConnect.session,
let walletInfo = session.walletInfo,
let walletAddress = walletInfo.accounts.first
else { return }
onDidConnect?(WalletDetails(address: walletAddress, chainId: walletInfo.chainId))
}
func didDisconnect() {
}
}
I am trying to connect VPN using OpenVPNAdapter but the PacketTunnelProvider isn't called from the controller. What am i missing here?
Controller.swift
import NetworkExtension
var providerManager: NETunnelProviderManager!
var provider = PacketTunnelProvider()
override func viewDidLoad() {
super.viewDidLoad()
self.loadProviderManager {
self.configureVPN(response: self.arrResponse[0], serverAddress: self.arrResponse[0].iP, username: "vpn", password: "vpn")
}
}
func loadProviderManager(completion:#escaping () -> Void) {
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if error == nil {
self.providerManager = managers?.first ?? NETunnelProviderManager()
completion()
}
}
}
func configureVPN(response:Response,serverAddress: String, username: String, password: String) {
let data = Data(base64Encoded:response.openVPNConfigDataBase64, options: .ignoreUnknownCharacters)
print(data!)
let decodedString = String(data: data!, encoding: .utf8)!
print(decodedString)
self.providerManager?.loadFromPreferences { error in
if error == nil {
let tunnelProtocol = NETunnelProviderProtocol()
tunnelProtocol.username = username
tunnelProtocol.serverAddress = serverAddress
tunnelProtocol.providerBundleIdentifier = "***.*****.********.***********.********"
tunnelProtocol.providerConfiguration = ["ovpn": data!, "username": username, "password": password]
tunnelProtocol.disconnectOnSleep = false
self.providerManager.protocolConfiguration = tunnelProtocol
self.providerManager.localizedDescription = "SMVPN"
self.providerManager.isEnabled = true
self.providerManager.saveToPreferences(completionHandler: { (error) in
if error == nil {
self.providerManager.loadFromPreferences(completionHandler: { (error) in
if error == nil {
self.provider.startTunnel(options: nil) { error in //this called here not in network extension
if error != nil {
print(error!)
}else {
}
}
}else {
print(error!.localizedDescription)
}
})
}
})
}
}
}
Project Entitlement
PacketTunnelProvider.swift
import NetworkExtension
import OpenVPNAdapter
class PacketTunnelProvider: NEPacketTunnelProvider {
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var vpnReachability = OpenVPNReachability()
var configuration: OpenVPNConfiguration!
var properties: OpenVPNConfigurationEvaluation!
var UDPSession: NWUDPSession!
var TCPConnection: NWTCPConnection!
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
override func startTunnel(options: [String : NSObject]?, completionHandler: #escaping (Error?) -> Void) {
// Add code here to start the process of connecting the tunnel.
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else { return }
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
do {
properties = try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
configuration.tunPersist = true
if !properties.autologin {
if let username: String = providerConfiguration["username"] as? String, let password: String = providerConfiguration["password"] as? String {
let credentials = OpenVPNCredentials()
credentials.username = username
credentials.password = password
do {
try vpnAdapter.provide(credentials: credentials)
} catch {
completionHandler(error)
return
}
}
}
vpnReachability.startTracking { [weak self] status in
guard status != .notReachable else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
vpnAdapter.connect(using: self)
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: #escaping () -> Void) {
// Add code here to start the process of stopping the tunnel.
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
// Add code here to handle the message.
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: #escaping () -> Void) {
// Add code here to get ready to sleep.
completionHandler()
}
override func wake() {
// Add code here to wake up.
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: #escaping (Error?) -> Void) {
setTunnelNetworkSettings(networkSettings) { (error) in
completionHandler(error == nil ? self.packetFlow as? Error : nil)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: #escaping (OpenVPNAdapterPacketFlow?) -> Void) {
setTunnelNetworkSettings(networkSettings) { (error) in
completionHandler(error == nil ? self.packetFlow : nil)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else {
return
}
NSLog("Error: \(error.localizedDescription)")
NSLog("Connection Info: \(vpnAdapter.connectionInformation.debugDescription)")
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
NSLog("Log: \(logMessage)")
}
}
extension PacketTunnelProvider: OpenVPNAdapterPacketFlow {
func readPackets(completionHandler: #escaping ([Data], [NSNumber]) -> Void) {
packetFlow.readPackets(completionHandler: completionHandler)
}
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
return packetFlow.writePackets(packets, withProtocols: protocols)
}
}
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
Extension Entitlement
self.provider.startTunnel(options: nil) { error in // This is called here, not in network extension
This method is called from controller but didn't get called in network extension.
I posted all my code, so if I missed something, then please let me know. Any help is appreciated.
I found this question but I haven't figured it out yet.
PacketTunnelProvider network extension not called Swift 3
You haven't mentioned anything about how you are testing network extension. You should attach a Network extension process before running your project for debugging. Then only network extension methods will trigger for debug.
Finally i found my mistake it's i tried to called network extension but it will never called. so i called NETunnelProviderManager directly and it will work.
self.providerManager.loadFromPreferences(completionHandler: { (error) in
if error == nil {
do {
try self.providerManager.connection.startVPNTunnel()
} catch let error {
print(error.localizedDescription)
}
}else {
print(error!.localizedDescription)
}
})
Please tell me, I have 2 classes. There is a load_shop() function that enables the display of the webView.
If I call a function from the first class ViewController then the function is executed and the webView is displayed, and if from another class CheckUpdate the function is executed (print is output) but the webView is not displayed. What could be the problem?
Thanks for any help. I'm new to this.
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
self.load_shop()
}
...
func load_shop() {
view = webView
print("finish_update")
}
}
class CheckUpdate: NSObject {
if (...) {
ViewController().load_shop()
}
}
ViewController.swift
import UIKit
import WebKit
import UserNotifications
import Foundation
class ViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.bounces = false;
let myURL = URL(string:"https://google.com")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest);
webView.navigationDelegate = self
}
var update = 0
func load_shop() {
view = webView
print("finish_update")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
CheckUpdate.shared.showUpdate(withConfirmation: false)
}
func close() {
exit(0);
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
CheckUpdate.swift
import Foundation
import UIKit
enum VersionError: Error {
case invalidBundleInfo, invalidResponse
}
class LookupResult: Decodable {
var results: [AppInfo]
}
class AppInfo: Decodable {
var version: String
var trackViewUrl: String
}
class CheckUpdate: NSObject {
let first_class = ViewController()
static let shared = CheckUpdate()
func showUpdate(withConfirmation: Bool) {
DispatchQueue.global().async {
self.checkVersion(force : !withConfirmation)
}
}
private func checkVersion(force: Bool) {
if let currentVersion = self.getBundle(key: "CFBundleShortVersionString") {
_ = getAppInfo { (info, error) in
if let appStoreAppVersion = info?.version {
if let error = error {
print("error getting app store version: ", error)
} else if appStoreAppVersion == currentVersion {
self.first_class.load_shop()
print("Already on the last app version: ",currentVersion)
} else {
print("Needs update: AppStore Version: \(appStoreAppVersion) > Current version: ",currentVersion)
DispatchQueue.main.async {
let topController: UIViewController = (UIApplication.shared.windows.first?.rootViewController)!
topController.showAppUpdateAlert(Version: (info?.version)!, Force: force, AppURL: (info?.trackViewUrl)!)
}
}
}
}
}
}
private func getAppInfo(completion: #escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
// You should pay attention on the country that your app is located, in my case I put Brazil */br/*
// Você deve prestar atenção em que país o app está disponível, no meu caso eu coloquei Brasil */br/*
guard let identifier = self.getBundle(key: "CFBundleIdentifier"),
let url = URL(string: "http://itunes.apple.com/br/lookup?bundleId=\(identifier)") else {
DispatchQueue.main.async {
completion(nil, VersionError.invalidBundleInfo)
}
return nil
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let error = error { throw error }
guard let data = data else { throw VersionError.invalidResponse }
let result = try JSONDecoder().decode(LookupResult.self, from: data)
print(result.results)
guard let info = result.results.first else {
throw VersionError.invalidResponse
}
completion(info, nil)
} catch {
completion(nil, error)
}
}
task.resume()
return task
}
func getBundle(key: String) -> String? {
guard let filePath = Bundle.main.path(forResource: "Info", ofType: "plist") else {
fatalError("Couldn't find file 'Info.plist'.")
}
// 2 - Add the file to a dictionary
let plist = NSDictionary(contentsOfFile: filePath)
// Check if the variable on plist exists
guard let value = plist?.object(forKey: key) as? String else {
fatalError("Couldn't find key '\(key)' in 'Info.plist'.")
}
return value
}
}
extension UIViewController {
#objc fileprivate func showAppUpdateAlert( Version : String, Force: Bool, AppURL: String) {
guard let appName = CheckUpdate.shared.getBundle(key: "CFBundleName") else { return } //Bundle.appName()
let alertTitle = "New version"
let alertMessage = "A new version of \(appName) are available on AppStore. Update now!"
let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
if !Force {
let notNowButton = UIAlertAction(title: "Not now", style: .default)
alertController.addAction(notNowButton)
}
let updateButton = UIAlertAction(title: "Update", style: .default) { (action:UIAlertAction) in
guard let url = URL(string: AppURL) else {
return
}
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
}
alertController.addAction(updateButton)
self.present(alertController, animated: true, completion: nil)
}
}
let first_class = ViewController() // stop doing this. It will not work. Think of it; you have a car (view controller), you want to turn on the radio of your car. What would you have to do? First turn your car on and then its radio right? But what you do in your code is; instead of turning on the radio of your car, you go and get a new car (like new instance of viewcontroller) and turn on the radio of the new car. But actually you need to turn on the radio of your own car not the new car. This how works instances in object oriented programming. You created an instance of a UIViewController (it is your car), then in another class you are creating another instance of UIViewController (that is not your car in the example). Then you try to modify the first one but actually you are modifying the another one. As soon as I am home I will modify your code from my laptop, because it is hard to edit it from the mobile phone.
Here is the modified code. NOTE THAT I just correct the parts where you want to call load_shop function from the CheckUpdate class. I mean I don't know all the logic behind your code so, if it contains more logical errors it's all up to you.
ViewController.swift
import UIKit
import WebKit
import UserNotifications
import Foundation // Not really necesary cause UIKit import already imports it
class ViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.bounces = false;
let myURL = URL(string:"https://google.com")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest);
webView.navigationDelegate = self
}
var update = 0
func load_shop() {
view = webView
print("finish_update")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
CheckUpdate.shared.showUpdate(withConfirmation: false, caller: self) // Attention here!!!
}
func close() {
exit(0);
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
CheckUpdate.swift
import UIKit
enum VersionError: Error {
case invalidBundleInfo, invalidResponse
}
class LookupResult: Decodable {
var results: [AppInfo]
}
class AppInfo: Decodable {
var version: String
var trackViewUrl: String
}
class CheckUpdate: NSObject {
let first_class: ViewController? // Attention here!!!
static let shared = CheckUpdate()
func showUpdate(withConfirmation: Bool, viewController: ViewController) {
first_class = viewController // Attention here!!!
DispatchQueue.global().async {
self.checkVersion(force : !withConfirmation)
}
}
private func checkVersion(force: Bool) {
if let currentVersion = self.getBundle(key: "CFBundleShortVersionString") {
_ = getAppInfo { (info, error) in
if let appStoreAppVersion = info?.version {
if let error = error {
print("error getting app store version: ", error)
} else if appStoreAppVersion == currentVersion {
self.first_class?.load_shop() // Atention here!!!
print("Already on the last app version: ",currentVersion)
} else {
print("Needs update: AppStore Version: \(appStoreAppVersion) > Current version: ",currentVersion)
DispatchQueue.main.async {
let topController: UIViewController = (UIApplication.shared.windows.first?.rootViewController)!
topController.showAppUpdateAlert(Version: (info?.version)!, Force: force, AppURL: (info?.trackViewUrl)!)
}
}
}
}
}
}
private func getAppInfo(completion: #escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
// You should pay attention on the country that your app is located, in my case I put Brazil */br/*
// Você deve prestar atenção em que país o app está disponível, no meu caso eu coloquei Brasil */br/*
guard let identifier = self.getBundle(key: "CFBundleIdentifier"),
let url = URL(string: "http://itunes.apple.com/br/lookup?bundleId=\(identifier)") else {
DispatchQueue.main.async {
completion(nil, VersionError.invalidBundleInfo)
}
return nil
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let error = error { throw error }
guard let data = data else { throw VersionError.invalidResponse }
let result = try JSONDecoder().decode(LookupResult.self, from: data)
print(result.results)
guard let info = result.results.first else {
throw VersionError.invalidResponse
}
completion(info, nil)
} catch {
completion(nil, error)
}
}
task.resume()
return task
}
func getBundle(key: String) -> String? {
guard let filePath = Bundle.main.path(forResource: "Info", ofType: "plist") else {
fatalError("Couldn't find file 'Info.plist'.")
}
// 2 - Add the file to a dictionary
let plist = NSDictionary(contentsOfFile: filePath)
// Check if the variable on plist exists
guard let value = plist?.object(forKey: key) as? String else {
fatalError("Couldn't find key '\(key)' in 'Info.plist'.")
}
return value
}
}
extension UIViewController {
#objc fileprivate func showAppUpdateAlert( Version : String, Force: Bool, AppURL: String) {
guard let appName = CheckUpdate.shared.getBundle(key: "CFBundleName") else { return } //Bundle.appName()
let alertTitle = "New version"
let alertMessage = "A new version of \(appName) are available on AppStore. Update now!"
let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
if !Force {
let notNowButton = UIAlertAction(title: "Not now", style: .default)
alertController.addAction(notNowButton)
}
let updateButton = UIAlertAction(title: "Update", style: .default) { (action:UIAlertAction) in
guard let url = URL(string: AppURL) else {
return
}
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
}
alertController.addAction(updateButton)
self.present(alertController, animated: true, completion: nil)
}
}
If your CheckUpdate class resides in a file other than ViewController's file this is not to correct way to do it. You can define a closure or protocol in ViewController then implement it in CheckUpdate class. Something like:
class ViewController: UIViewController {
...
typeAlias OnUpdate = () -> Void
...
override func viewDidLoad() {
super.viewDidLoad()
self.load_shop()
}
...
let checkupdate = CheckUpdate(){
view = webView
print("finish_update")
}
}
class CheckUpdate: NSObject {
...
var onupdate:OnUpdate?
...
init(_ onupdate: OnUpdate?){
self.onupdate = onupdate
}
...
if (...) {
//ViewController().load_shop()
onupdate?()
}
}
I am working on a personal project where I will be connecting the user to a VPN. I have followed two blog posts on doing this which are https://medium.com/better-programming/how-to-build-an-openvpn-client-on-ios-c8f927c11e80 & https://kean.blog/post/vpn-configuration-manager. I was able to configure the VPN settings but still, it does not connect. Here are the images for my project Signing & Capabilities one for the App and the other is for the network extension.
my ViewController looks like this. There is a folder in the user app document directory that is named user which gets downloaded from the internet and these are the files that are in the folder. I am not sure if I am supposed to use it other than the user.ovpn file
import UIKit
import Zip
import Alamofire
import NetworkExtension
class ViewController: UIViewController {
var selectedServer: Server?
var providerManager: NETunnelProviderManager!
override func viewDidLoad() {
super.viewDidLoad()
self.loadProviderManager {
self.configureVPN(serverAddress: "openvpn://\(self.selectedServer!.server.address):\(self.selectedServer!.server.port)", username: "\(self.username)", password: "\(self.password)")
}
}
func loadProviderManager(completion:#escaping () -> Void) {
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if error == nil {
self.providerManager = managers?.first ?? NETunnelProviderManager()
completion()
}
}
}
func configureVPN(serverAddress: String, username: String, password: String) {
guard let configData = self.readFile(path: "user/user.ovpn") else { return }
self.providerManager?.loadFromPreferences { error in
if error == nil {
let tunnelProtocol = NETunnelProviderProtocol()
tunnelProtocol.username = username
tunnelProtocol.serverAddress = serverAddress
tunnelProtocol.providerBundleIdentifier = "com.example.Networking.tunnel"
tunnelProtocol.providerConfiguration = ["ovpn": configData, "username": username, "password": password]
tunnelProtocol.disconnectOnSleep = false
self.providerManager.protocolConfiguration = tunnelProtocol
self.providerManager.localizedDescription = "OpenVPN"
self.providerManager.isEnabled = true
self.providerManager.saveToPreferences(completionHandler: { (error) in
if error == nil {
self.providerManager.loadFromPreferences(completionHandler: { (error) in
do {
try self.providerManager.connection.startVPNTunnel()
} catch let error {
print(error.localizedDescription)
}
})
}
})
}
}
}
func readFile(path: String) -> Data? {
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = documentDirectory.appendingPathComponent(path)
return try Data(contentsOf: fileURL, options: .uncached)
}
catch let error {
print(error.localizedDescription)
}
return nil
}
}
And my PacketTunnelProvider looks like this
import NetworkExtension
import OpenVPNAdapter
class PacketTunnelProvider: NEPacketTunnelProvider {
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var vpnReachability = OpenVPNReachability()
var configuration: OpenVPNConfiguration!
var properties: OpenVPNProperties!
var UDPSession: NWUDPSession!
var TCPConnection: NWTCPConnection!
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
override func startTunnel(options: [String : NSObject]?, completionHandler: #escaping (Error?) -> Void) {
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else { return }
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
do {
properties = try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
configuration.tunPersist = true
if !properties.autologin {
if let username: String = providerConfiguration["username"] as? String, let password: String = providerConfiguration["password"] as? String {
let credentials = OpenVPNCredentials()
credentials.username = username
credentials.password = password
do {
try vpnAdapter.provide(credentials: credentials)
} catch {
completionHandler(error)
return
}
}
}
vpnReachability.startTracking { [weak self] status in
guard status != .notReachable else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
vpnAdapter.connect()
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: #escaping () -> Void) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: #escaping () -> Void) {
completionHandler()
}
override func wake() {
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: #escaping (OpenVPNAdapterPacketFlow?) -> Void) {
setTunnelNetworkSettings(networkSettings) { (error) in
completionHandler(error == nil ? self.packetFlow : nil)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else {
return
}
NSLog("Error: \(error.localizedDescription)")
NSLog("Connection Info: \(vpnAdapter.connectionInformation.debugDescription)")
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
NSLog("Log: \(logMessage)")
}
}
extension PacketTunnelProvider: OpenVPNAdapterPacketFlow {
func readPackets(completionHandler: #escaping ([Data], [NSNumber]) -> Void) {
packetFlow.readPackets(completionHandler: completionHandler)
}
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
return packetFlow.writePackets(packets, withProtocols: protocols)
}
}
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
i was wondering what's the right design to:
Make an API call
Create a model of the result
Load the image of the model
Wait for everything to be loaded ..and then update the ui
I used (nested) delegation and a dispatch_group. But i am sure, you might have some tips how to do this right, don't you?
Here is my working code:
View Controller
#IBAction func loadLoaction(sender: AnyObject) {
placeManager.loadRandomLoaction(lat, longitude: long)
}
func placeDidLoad(place: Place) {
nameLabel.text = place.name
imageView.image = place.image
}
Classes
import Foundation
import Alamofire
extension Alamofire.Request {
class func imageResponseSerializer() -> Serializer {
return { request, response, data in
if data == nil {
return (nil, nil)
}
let image = UIImage(data: data!, scale: UIScreen.mainScreen().scale)
return (image, nil)
}
}
func responseImage(completionHandler: (NSURLRequest, NSHTTPURLResponse?, UIImage?, NSError?) -> Void) -> Self {
return response(serializer: Request.imageResponseSerializer(), completionHandler: { (request, response, image, error) in
completionHandler(request, response, image as? UIImage, error)
})
}
}
public protocol PlaceDelegate: class {
func placeDidLoad(place: Place)
}
public class Place : NSObject {
var delegate: PlaceDelegate?
public var name: String?
public var category: String?
public var image_url: String?
public var snippet_text: String?
public var address: String?
public var distance: String?
public var url: String?
public var image: UIImage = UIImage()
var dispatch_group = dispatch_group_create()
init(fromResponseDict responseDict: Dictionary<String, String>, delegate: PlaceDelegate) {
self.delegate = delegate
// Set props
self.name = responseDict["name"]
self.category = responseDict["category"]
self.image_url = responseDict["image_url"]!
self.snippet_text = responseDict["snippet_text"]
self.url = responseDict["url"]
self.address = responseDict["address"]!
super.init()
// Load image
self.loadImage()
// Dispatch if success
dispatch_group_notify(self.dispatch_group, dispatch_get_main_queue(), {
self.delegate?.placeDidLoad(self)
})
}
// (3) Load the image
func loadImage() {
dispatch_group_enter(self.dispatch_group);
var request:Alamofire.Request = Alamofire.request(.GET, self.image_url!).responseImage() {
(request, _, image, error) in
if error == nil && image != nil {
NSLog("imageRequestSuccess")
self.image = image!
// Dispatch if success
dispatch_group_leave(self.dispatch_group)
} else {
NSLog("imageRequestFailure")
// Dispatch also to handle failure
dispatch_group_leave(self.dispatch_group)
}
}
}
}
public protocol PlaceManagerDelegate: class {
func placeDidLoad(place: Place)
}
public class PlaceManager : NSObject, PlaceDelegate {
public weak var delegate: PlaceManagerDelegate?
var isLoading = false
enum Router: URLRequestConvertible {
static let baseURLString = "http://api.domain.com"
// Endpoints
case RandomPlace(lat:Double, long:Double)
var URLRequest: NSURLRequest {
let (path: String, parameters: [String: AnyObject]) = {
switch self {
case .RandomPlace (let lat, let long):
let params : [ String : AnyObject] = ["ll": "\(lat),\(long)", "", "debug":"true"]
return ("/getRandomLocation", params)
}
}()
let URL = NSURL(string: Router.baseURLString)
let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path))
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: parameters).0
}
}
public func loadRandomLoaction(latitude:Double, longitude:Double) {
if isLoading {
return
}
isLoading = true
// (1) Make the API Call
Alamofire.request(PlaceManager.Router.RandomPlace(lat: latitude, long: longitude)).responseJSON() {
(_, _, JSON, error) in
if error == nil {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
if let responseDict = JSON as? Dictionary<String, String> {
// (2) Create the model
let place = Place(fromResponseDict: responseDict, delegate:self)
}
}
} else {
NSLog(error!.localizedDescription)
}
self.isLoading = false
}
}
// (4) Update the UI
public func placeDidLoad(place: Place) {
self.delegate?.placeDidLoad(place)
}
}