본문 바로가기

애플리케이션 개발/HRC(Home Remote Control)

2-2. iOS에서 Web Scraping해서 정보 가져오기

나의 목표는 iOS Application에서 HTML 문서를 읽어와 버튼의 상태를 읽고 그 상태를 iOS Application의 버튼에 띄워주는 것이다. 

구체적으로 정리를 해보면, 다음과 같다. 

 

1. Application이 시작할 때, HTML 문서 읽기

2. HTML 문서의 버튼 상태 저장하기

3. iOS Application의 컨트롤에 상태 나타내기

 

먼저 Application이 시작할 때, HTML 문서를 읽어주기 위하여 AppDelegate.swift파일에 클래스 변수로 url과 buttonState를 추가하고, application 함수를 수정해 주었다. 

url은 반복적으로 사용되므로 ConentView 클래스에 있던 것은 제거를 하고 AppDelegate 클래스에서 한 번만 선언하기로 했다.(taeminator1.tistory.com/13 참고)

buttonState는 Bool 배열로 선언했다. 지금은 버튼이 2개로 고정되어 있으므로 초기값을 false로 설정해두었다. 

그리고 Application이 launch 되자마자 실행되는 application 함수에서 HTML문을 불러오고 원하는 텍스트를 추출해 buttonState 변수에 저장했다. 

 

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    static var buttonState: [Bool] = [false, false]
    static let url: String = "http://yourID.iptime.org"

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        var index: Int = 0
        let task = URLSession.shared.dataTask(with: URL(string: AppDelegate.url)!) {
            (data, response, error) in
            
            let hStr = String(data: data!, encoding: .utf8)!
            for i in 0 ... hStr.count {
                // HTML문에서 "label 찾기
                if hStr[hStr.index(hStr.startIndex, offsetBy: i)] == "\"" &&
                    hStr[hStr.index(hStr.startIndex, offsetBy: i + 1)] == "l" &&
                    hStr[hStr.index(hStr.startIndex, offsetBy: i + 2)] == "a" &&
                    hStr[hStr.index(hStr.startIndex, offsetBy: i + 3)] == "b" &&
                    hStr[hStr.index(hStr.startIndex, offsetBy: i + 4)] == "e" &&
                    hStr[hStr.index(hStr.startIndex, offsetBy: i + 5)] == "l" {
                
                    let range = hStr.index(hStr.startIndex, offsetBy: i + 10)..<hStr.index(hStr.startIndex, offsetBy: i + 15)
                
                    if hStr[range] == "true " {
                        AppDelegate.buttonState[index] = true
                        index += 1
                        
                        if index == 2 { break } else { continue }
                    } else {
                        AppDelegate.buttonState[index] = false
                        index += 1
                        
                        if index == 2 { break } else { continue }
                    }
                }
            }
        }
        task.resume()
            
        return true
    }

    ...

}

사실 내가 원하는 HTML문의 범위는 정해져 있을 것이므로 for문의 범위를 0 ... hStr.count로 할 필요는 전혀 없다. 

 

다음으로 buttonState에 저장된 값들을 다음과 같이 ToggleView를 생성할 때, 반영하면 된다. 

 

ContentView.swift

import SwiftUI
import WebKit

struct ContentView: View {

    static let wkWebView = WKWebView()
    static let request: URLRequest = URLRequest.init(url: NSURL.init(string: AppDelegate.url)! as URL)
    
    var body: some View {
        
        HStack {
            VStack {
                ScrollView {
                    ForEach(0 ..< 2) { index in
                        ToggleView(isChecked: AppDelegate.buttonState[index], index: index)
                    }
                }
            }
            .padding(.all, 20.0)
            
            VStack {
                Spacer()
                Button(action: refreshButtonClicked) {
                    Text("refresh")
                }
                .frame(width: 150)
            }
        }
    }
    
    ...
    
}

 

그런데, 이렇게만 변경하면, 저장된 상태가 반영이 될 수도 있고(?) 안 될 수도 있다(?). 

그 이유는 Application이 실행된 이후, AppDelegate.swift에서 HTML 문서를 읽는 도중 Application의 컨트롤을 띄울 수도 있기 때문이다. 이러한 현상은, HTML문을 읽는 let task = ~ 부분이 일반적인 Queue에 저장되어 실행이 끝나면 다음 코드로 넘어가는 것이 아니라 Thread를 통해 HTML문을 읽는 도중 다른 작업이 수행될 수 있도록 했기 때문이 아닐까 하고 추측해본다. 

이러한 현상을 방지하기 위해서 임의로 Application의 화면이 띄워지기 전에 시간을 두어 HTML문의 Data 추출이 끝나고 Application의 컨트롤을 띄우도록 만들었다.

화면을 띄우는 부분을 DispatchQueue.main.asyncAfter함수로 감싸주기만 하면 된다. 시간은 너무 길면, Application의 화면이 늦게 띄워지고, 너무 짧으면, Data를 가져올 시간이 부족해서, 경험적으로 1s로 잡았다. 

 

SceneDelegate.swift

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // 페이지가 로딩되기 전 1초 기다림(HTML 문서를 읽는 시간 벌기)
        // 페이지가 로딩안 될 시 오류 처리해야 함
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            let contentView = ContentView()

            // Use a UIHostingController as window root view controller.
            if let windowScene = scene as? UIWindowScene {
                let window = UIWindow(windowScene: windowScene)
                window.rootViewController = UIHostingController(rootView: contentView)
                self.window = window
                window.makeKeyAndVisible()
            }
        }
    }

    ...


}

 

Xcode에서 컴파일을 한 후, 확인해 보자. 아래 영상과 같이, 서버의 txt 파일이 true/true였기 때문에, Toggle이 전부 On 상태로 화면에 배치가 되었다. 그리고 Toggle을 누를 때마다 txt 파일이 갱신되고, false/true 상태로 Application을 다시 실행시키면 첫 번째 Toggle은 Off, 두 번째는 On으로 되어 화면에 나타난다.

 

 

 

마찬가지로 추후 해결해야 할 몇 가지 문제가 있다. 

  • 버튼을 연속적으로 누를 경우, 서버에 반영이 안 되는 경우가 생긴다. 현재는 버튼의 상태와 관계없이 Swift나 JavaScript를 작성하였는데, Toggle의 상태가 True일 때와 False일 때를 나눠서 함수를 각각 작성해서 해결할 수 있을 것 같다. 
  • 서버에 문제가 생겨 Web page가 열리지 않을 때에 대한 예외상황을 정의하지 않았다. 

이제 iOS Application과 서버 간의 통신은 어느 정도 되었다. 이제 서버와 MCU 간의 통신이 살펴봐야 한다.