패키지 선언
import OneS1ght
#import "MyApp-Swift.h" // MyApp = Product Module Name
초기화
.initialize
OneS1ght API 키로 측위엔진을 초기화합니다.try await OneS1ght.initialize(sdkKey: "YOUR_SDK_KEY")
[OneS1ghtBridge initializeWithSdkKey:@"YOUR_SDK_KEY" completion:^(NSError *error) { }];
.requestPermission
측위엔진 사용을 위한 앱 권한을 요청합니다.| 결과 | 뜻 |
|---|---|
authorized | 측위 가능 |
denied | 권한 거부됨 |
unsupported | 측위 미지원 기기 |
let status = await OneS1ght.requestPermission()
[OneS1ghtBridge requestPermission:^(NSString *status) { }]; // @"authorized" · @"denied" · @"unsupported"
.setLanguage
SDK 로그 문구 언어를 설정합니다.ko : 한국어, ja : 일본어, en : 영어, nil : 시스템 설정 언어
OneS1ght.setLanguage("ko")
[OneS1ghtBridge setLanguage:@"ko"];
.onDebugLog
SDK 로그를 수신합니다.ko : 로그 레벨, message : 로그 메세지
OneS1ght.onDebugLog = { level, message in
if level >= .warn { print("[OneS1ght] \(message)") }
}
[OneS1ghtBridge onDebugLog:^(NSString *level, NSString *message) { }];
.reset
로케이터의 세션을 초기화합니다.await OneS1ght.reset()
[OneS1ghtBridge reset:^{ }];
상태 조회
.isInitialized
초기화 성공 확인 여부를 반환합니다.let ready = OneS1ght.isInitialized
BOOL ready = OneS1ghtBridge.isInitialized;
.deviceAvailability
측위 가능 기기를 판별하고 미지원시 사유를 반환합니다.| 값 | 뜻 |
|---|---|
available | 측위 가능 |
osVersionTooLow | iOS 27 미만 |
deviceNotSupported | UWB 미지원 기기 |
switch OneS1ght.deviceAvailability {
case .available: break
case .osVersionTooLow: break
case .deviceNotSupported: break
@unknown default: break
}
NSString *availability = OneS1ghtBridge.deviceAvailability; // @"available" · @"osVersionTooLow" · @"deviceNotSupported"
.googleMapKey
도면을 표시할 Google Maps API 키를 반환합니다.let mapKey = OneS1ght.googleMapKey // String?
NSString *mapKey = OneS1ghtBridge.googleMapKey;
.sdkVersion
사용중인 SDK 버전을 반환합니다.let version = OneS1ght.sdkVersion // "0.2.3"
NSString *version = OneS1ghtBridge.sdkVersion;
프로필
.createProfile
새로운 개인화 프로필을 생성합니다. 성공적으로 생성하면 고유 프로필 ID를 반환합니다.let profileId = try await OneS1ght.createProfile(["gender": "F", "ageBand": "20s"])
[OneS1ghtBridge createProfile:@{ @"gender": @"F", @"ageBand": @"20s" }
completion:^(NSString *profileId, NSError *error) { }];
.identify
수집할 측위 데이터를 묶을 프로필을 연결합니다. 연결 해제시nil으로 설정하세요.
OneS1ght.identify(profileId: profileId)
[OneS1ghtBridge identifyWithProfileId:profileId];
.fetchProfile
프로필의 속성을 조회합니다.let attributes = try await OneS1ght.fetchProfile(profileId)
[OneS1ghtBridge fetchProfile:profileId
completion:^(NSDictionary<NSString *, NSString *> *attributes, NSError *error) { }];
.replaceProfile
프로필 속성을 교체하며 이전 데이터는 삭제됩니다.try await OneS1ght.replaceProfile(profileId, attributes: ["gender": "F", "ageBand": "30s"])
[OneS1ghtBridge replaceProfile:profileId
attributes:@{ @"gender": @"F", @"ageBand": @"30s" }
completion:^(NSError *error) { }];
.deleteProfile
프로필 정보를 영구 삭제합니다.try await OneS1ght.deleteProfile(profileId)
[OneS1ghtBridge deleteProfile:profileId completion:^(NSError *error) { }];
공간 조회
.buildings
테넌트와 연관된 모든 건물 목록을 조회합니다.let buildings = try await OneS1ght.buildings()
let building = try await OneS1ght.building(id: buildingId)
[OneS1ghtBridge buildings:^(NSArray<NSDictionary *> *buildings, NSError *error) { }]; // id · name · floorCount
.floors
건물의 층 목록을 조회합니다.let floors = try await OneS1ght.floors(buildingId: buildingId)
[OneS1ghtBridge floorsWithBuildingId:buildingId
completion:^(NSArray<NSDictionary *> *floors, NSError *error) { }];
.floor
선택한 층의 상세 정보를 조회합니다.let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
let png = floor.image // Data?
[OneS1ghtBridge floorWithBuildingId:buildingId floorId:floorId
completion:^(NSDictionary *floor, NSError *error) {
NSData *png = floor[@"image"];
}];
.zones
선택한 층의 모든 구역을 조회합니다.let zones = try await OneS1ght.zones(buildingId: buildingId, floorId: floorId)
let zone = try await OneS1ght.zone(buildingId: buildingId, floorId: floorId, zoneId: zoneId)
[OneS1ghtBridge zonesWithBuildingId:buildingId floorId:floorId
completion:^(NSArray<NSDictionary *> *zones, NSError *error) { }]; // id · name · polygon · dwellSeconds
.locators
선택한 층의 로케이터 정보를 조회합니다.let floorLocators = try await OneS1ght.locators(buildingId: buildingId, floorId: floorId)
let ready = floorLocators.positioningReady
[OneS1ghtBridge locatorsWithBuildingId:buildingId floorId:floorId
completion:^(NSDictionary *floorLocators, NSError *error) { }]; // locators · sessionId
층 지정
.setFloorMap
측위를 시작할 층을 지정합니다.let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
try await OneS1ght.setFloorMap(floor, buildingId: buildingId)
[OneS1ghtBridge setFloorMapWithBuildingId:buildingId floorId:floorId
completion:^(NSError *error) { }]; // floorId nil = 해제
.refreshZones
최신화된 구역 정보를 조회합니다.let zones = await OneS1ght.refreshZones()
[OneS1ghtBridge refreshZones:^(NSArray<NSDictionary *> *zones) { }];
측위 세션
.floorSession
선택한 층의 로케이터 세션을 획득합니다.let session = try OneS1ght.floorSession()
// 브리지가 세션을 감쌉니다 — 따로 가져오지 않습니다.
.begin
사용자 측위를 시작합니다.try await session.begin()
[OneS1ghtBridge beginWithCompletion:^(NSError *error) { }];
.end
사용자 측위를 종료합니다.await session.end()
[OneS1ghtBridge endWithCompletion:^{ }];
.pause · .resume
사용자 측위를 중지, 재개합니다. 종료와 달리 SDK 내 측위 엔진은 종료하지 않습니다.session.pause()
session.resume()
[OneS1ghtBridge pause];
[OneS1ghtBridge resume];
.isRunning · .isPaused
측위 상태여부와 중지여부를 반환합니다.let running = session.isRunning
let paused = session.isPaused
let current = session.floor // Floor?
BOOL running = OneS1ghtBridge.isRunning;
BOOL paused = OneS1ghtBridge.isPaused;
세션 이벤트
.onPosition
실시간으로 사용자 좌표 반환 이벤트를 발생합니다. (기본값 1초당 4회)session.onPosition = { coordinates in print(coordinates.x, coordinates.y) }
[OneS1ghtBridge onPosition:^(double x, double y, double z) { }];
.onZoneEnter · .onZoneExit
구역 진입, 이탈시 이벤트가 동작합니다.session.onZoneEnter = { zone in print("IN \(zone.name)") }
session.onZoneExit = { zone in print("OUT \(zone.name)") }
[OneS1ghtBridge onZoneEnter:^(NSDictionary *zone) { }];
[OneS1ghtBridge onZoneExit:^(NSDictionary *zone) { }];
.onZoneDwell
특정 구역의 체류 시간이 초과할 경우(dwellSeconds 기준) 이벤트가 발생합니다.
session.onZoneDwell = { zone, seconds in print("DWELL \(zone.name) \(seconds)s") }
[OneS1ghtBridge onZoneDwell:^(NSDictionary *zone, double seconds) { }];
.onTriggers
공간 내 이벤트(또는 시책) 동작시 이벤트가 발생합니다.session.onTriggers = { zoneId, triggers in
for trigger in triggers { print(trigger.type, trigger.payload ?? [:]) }
}
[OneS1ghtBridge onTriggers:^(NSString *zoneId, NSArray<NSDictionary *> *triggers) { }]; // triggerId · type · payload
.onFloorDetected
로케이터의 BLE 신호를 수신한 경우 이벤트가 발생합니다.session.onFloorDetected = { floorId in
guard let floorId else { return }
Task {
let floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
try await OneS1ght.setFloorMap(floor, buildingId: buildingId)
}
}
[OneS1ghtBridge onFloorDetected:^(NSString *floorId) {
if (!floorId) { return; }
[OneS1ghtBridge setFloorMapWithBuildingId:buildingId floorId:floorId completion:^(NSError *error) { }];
}];
.onStopped
사용자 측위가 중지된 경우 이벤트가 발생합니다.| 이유 | 뜻 |
|---|---|
ended | end() · reset · 키 교체 |
engineFailed | 측위엔진 구동 실패시 |
session.onStopped = { reason in
switch reason {
case .ended: break
case .engineFailed: break
@unknown default: break
}
}
[OneS1ghtBridge onStopped:^(NSString *reason) { }]; // @"ended" · @"engineFailed"
.onConfigChanged
콘솔 페이지에서 설정이 변경되었을 경우 이벤트가 발생합니다.| 신호 | 의미 |
|---|---|
zonesChanged(floorId) | 구역 변경 |
planChanged(floorId) | 도면 변경 |
rulesChanged(zoneId) | 시책 변경 |
sdkConfigChanged | 원격 설정 변경 |
resyncNeeded | 재연결 · 이벤트 누락 |
session.onConfigChanged = { change in
switch change {
case .zonesChanged, .resyncNeeded: Task { _ = await OneS1ght.refreshZones() }
case .planChanged(let floorId): print("redraw", floorId ?? "")
default: break
}
}
[OneS1ghtBridge onConfigChanged:^(NSString *kind, NSString *targetId) {
if ([kind isEqualToString:@"zonesChanged"]) {
[OneS1ghtBridge refreshZones:^(NSArray<NSDictionary *> *zones) { }];
}
}];
데이터 전송
좌표는 300건 또는 60초마다, 그리고 백그라운드 진입과end() 때 자동 전송됩니다.
.uploadPendingPositions
쌓인 좌표 즉시 전송.await OneS1ght.uploadPendingPositions()
[OneS1ghtBridge uploadPendingPositions:^{ }];
.discardPendingPositions
쌓인 좌표 폐기.OneS1ght.discardPendingPositions()
[OneS1ghtBridge discardPendingPositions];
오류 처리
실패는SdkError(SDK 상태) 와 ApiError(통신) 로 옵니다. 코드의 뜻은 에러 코드를 참고하세요. Objective-C 는 NSError.userInfo[@"sdkCode"] 에 담깁니다.
do {
try await OneS1ght.initialize(sdkKey: key)
} catch let e as SdkError {
print(e.code.rawValue) // "E1003"
} catch let e as ApiError {
print(e.code.rawValue, e.description) // "E5005" · 사유
}
[OneS1ghtBridge initializeWithSdkKey:key completion:^(NSError *error) {
NSString *code = error.userInfo[@"sdkCode"]; // @"E1003"
}];
모델
목록은[T], 바이트는 Data 입니다.
Coordinates
| 필드 | 타입 | 설명 |
|---|---|---|
x · y | Double | 도면 로컬 미터 |
z | Double | 높이 |
Building · Floor
| 타입 | 필드 | 설명 |
|---|---|---|
Building | id · name | 건물 ID · 이름 |
floorCount: Int? | 층 수 | |
Floor | id · name | 층 ID · 이름 |
image: Data? | 도면 이미지 (BASE64) | |
hasPlan: Bool | 도면 등록 여부 | |
originX · originY | 도면 원점(미터) | |
widthM · heightM | 도면 크기(미터) | |
minX · minY · maxX · maxY | 배치 범위 |
Locator · FloorLocators
| 타입 | 필드 | 설명 |
|---|---|---|
Locator | address: Int | UWB MAC 뒤 2바이트 |
x · y · z | 도면 로컬 미터 | |
FloorLocators | locators | 층의 로케이터 목록 |
sessionId: Int? | UWB 세션 — 층마다 다름 | |
positioningReady: Bool | 로케이터와 세션이 모두 있는가 |
Zone
| 필드 | 타입 | 설명 |
|---|---|---|
id · name | String | 구역 ID · 이름 |
polygon | [Position] | 꼭짓점(x, y 미터) |
dwellSeconds | Int? | 체류 이벤트 기준(초) |
Trigger
| 필드 | 타입 | 설명 |
|---|---|---|
triggerId | String | 트리거 ID |
type | String | signage · coupon · tracking · merch · generic |
payload | [String: String]? | 타입별 부가 정보 |
Objective-C 브리지
아래 파일을 앱 타깃에 추가하고 한 번 빌드하면<Product Module Name>-Swift.h 가 생깁니다.
OneS1ghtBridge.swift
// OneS1ghtBridge.swift: Objective-C 에서 OneS1ght 를 부르기 위한 브리지
import Foundation
import OneS1ght
@MainActor @objc public final class OneS1ghtBridge: NSObject {
// MARK: 초기화
@objc public static func initialize(sdkKey: String, completion: @escaping (NSError?) -> Void) {
Task {
do { try await OneS1ght.initialize(sdkKey: sdkKey); completion(nil) }
catch { completion(nsError(error)) }
}
}
/// status: "authorized" | "denied" | "unsupported"
@objc public static func requestPermission(_ completion: @escaping (String) -> Void) {
Task { completion(String(describing: await OneS1ght.requestPermission())) }
}
@objc public static func setLanguage(_ code: String?) {
OneS1ght.setLanguage(code)
}
@objc public static func reset(_ completion: @escaping () -> Void) {
Task { await OneS1ght.reset(); completion() }
}
/// level: "log" | "info" | "warn" | "error"
@objc public static func onDebugLog(_ handler: @escaping (String, String) -> Void) {
OneS1ght.onDebugLog = { level, message in handler(String(describing: level), message) }
}
// MARK: 상태
@objc public static var isInitialized: Bool { OneS1ght.isInitialized }
/// "available" | "osVersionTooLow" | "deviceNotSupported"
@objc public static var deviceAvailability: String { String(describing: OneS1ght.deviceAvailability) }
@objc public static var isDeviceAvailable: Bool { OneS1ght.isDeviceAvailable }
@objc public static var googleMapKey: String? { OneS1ght.googleMapKey }
@objc public static var sdkVersion: String { OneS1ght.sdkVersion }
// MARK: 프로필
@objc public static func createProfile(_ attributes: [String: String],
completion: @escaping (String?, NSError?) -> Void) {
Task {
do { completion(try await OneS1ght.createProfile(attributes), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func fetchProfile(_ profileId: String,
completion: @escaping ([String: String]?, NSError?) -> Void) {
Task {
do { completion(try await OneS1ght.fetchProfile(profileId), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func replaceProfile(_ profileId: String, attributes: [String: String],
completion: @escaping (NSError?) -> Void) {
Task {
do { try await OneS1ght.replaceProfile(profileId, attributes: attributes); completion(nil) }
catch { completion(nsError(error)) }
}
}
@objc public static func deleteProfile(_ profileId: String, completion: @escaping (NSError?) -> Void) {
Task {
do { try await OneS1ght.deleteProfile(profileId); completion(nil) }
catch { completion(nsError(error)) }
}
}
@objc public static func identify(profileId: String?) {
OneS1ght.identify(profileId: profileId)
}
// MARK: 공간 조회 — 모델은 NSDictionary 로 넘긴다
@objc public static func buildings(_ completion: @escaping ([[String: Any]]?, NSError?) -> Void) {
Task {
do { completion(try await OneS1ght.buildings().map(dict), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func building(id: String, completion: @escaping ([String: Any]?, NSError?) -> Void) {
Task {
do { completion(dict(try await OneS1ght.building(id: id)), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func floors(buildingId: String,
completion: @escaping ([[String: Any]]?, NSError?) -> Void) {
Task {
do { completion(try await OneS1ght.floors(buildingId: buildingId).map(dict), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func floor(buildingId: String, floorId: String,
completion: @escaping ([String: Any]?, NSError?) -> Void) {
Task {
do { completion(dict(try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func zones(buildingId: String, floorId: String,
completion: @escaping ([[String: Any]]?, NSError?) -> Void) {
Task {
do { completion(try await OneS1ght.zones(buildingId: buildingId, floorId: floorId).map(dict), nil) }
catch { completion(nil, nsError(error)) }
}
}
@objc public static func zone(buildingId: String, floorId: String, zoneId: String,
completion: @escaping ([String: Any]?, NSError?) -> Void) {
Task {
do {
let zone = try await OneS1ght.zone(buildingId: buildingId, floorId: floorId, zoneId: zoneId)
completion(dict(zone), nil)
} catch { completion(nil, nsError(error)) }
}
}
@objc public static func locators(buildingId: String, floorId: String,
completion: @escaping ([String: Any]?, NSError?) -> Void) {
Task {
do { completion(dict(try await OneS1ght.locators(buildingId: buildingId, floorId: floorId)), nil) }
catch { completion(nil, nsError(error)) }
}
}
// MARK: 층 지정
/// floorId 가 nil 이면 층 지정을 비운다
@objc public static func setFloorMap(buildingId: String?, floorId: String?,
completion: @escaping (NSError?) -> Void) {
Task {
do {
var floor: Floor?
if let buildingId, let floorId {
floor = try await OneS1ght.floor(buildingId: buildingId, floorId: floorId)
}
try await OneS1ght.setFloorMap(floor, buildingId: buildingId)
completion(nil)
} catch { completion(nsError(error)) }
}
}
@objc public static func refreshZones(_ completion: @escaping ([[String: Any]]) -> Void) {
Task { completion(await OneS1ght.refreshZones().map(dict)) }
}
// MARK: 측위 세션
@objc public static func begin(completion: @escaping (NSError?) -> Void) {
Task {
do { try await OneS1ght.floorSession().begin(); completion(nil) }
catch { completion(nsError(error)) }
}
}
@objc public static func end(completion: @escaping () -> Void) {
Task {
if let session = try? OneS1ght.floorSession() { await session.end() }
completion()
}
}
@objc public static func pause() { (try? OneS1ght.floorSession())?.pause() }
@objc public static func resume() { (try? OneS1ght.floorSession())?.resume() }
@objc public static var isRunning: Bool { (try? OneS1ght.floorSession())?.isRunning ?? false }
@objc public static var isPaused: Bool { (try? OneS1ght.floorSession())?.isPaused ?? false }
// MARK: 세션 이벤트
/// 도면 로컬 미터
@objc public static func onPosition(_ handler: @escaping (Double, Double, Double) -> Void) {
(try? OneS1ght.floorSession())?.onPosition = { c in handler(c.x, c.y, c.z) }
}
@objc public static func onZoneEnter(_ handler: @escaping ([String: Any]) -> Void) {
(try? OneS1ght.floorSession())?.onZoneEnter = { zone in handler(dict(zone)) }
}
@objc public static func onZoneExit(_ handler: @escaping ([String: Any]) -> Void) {
(try? OneS1ght.floorSession())?.onZoneExit = { zone in handler(dict(zone)) }
}
@objc public static func onZoneDwell(_ handler: @escaping ([String: Any], Double) -> Void) {
(try? OneS1ght.floorSession())?.onZoneDwell = { zone, seconds in handler(dict(zone), seconds) }
}
/// triggers: [{ triggerId, type, payload }]
@objc public static func onTriggers(_ handler: @escaping (String, [[String: Any]]) -> Void) {
(try? OneS1ght.floorSession())?.onTriggers = { zoneId, triggers in
handler(zoneId, triggers.map { t in
var d: [String: Any] = ["triggerId": t.triggerId, "type": t.type]
if let payload = t.payload { d["payload"] = payload }
return d
})
}
}
@objc public static func onFloorDetected(_ handler: @escaping (String?) -> Void) {
(try? OneS1ght.floorSession())?.onFloorDetected = { floorId in handler(floorId) }
}
/// reason: "ended" | "engineFailed"
@objc public static func onStopped(_ handler: @escaping (String) -> Void) {
(try? OneS1ght.floorSession())?.onStopped = { reason in handler(String(describing: reason)) }
}
/// kind: "zonesChanged" | "planChanged" | "rulesChanged" | "sdkConfigChanged" | "resyncNeeded"
/// targetId: zonesChanged·planChanged 는 층 ID, rulesChanged 는 구역 ID
@objc public static func onConfigChanged(_ handler: @escaping (String, String?) -> Void) {
(try? OneS1ght.floorSession())?.onConfigChanged = { change in
switch change {
case .zonesChanged(let floorId): handler("zonesChanged", floorId)
case .planChanged(let floorId): handler("planChanged", floorId)
case .rulesChanged(let zoneId): handler("rulesChanged", zoneId)
case .sdkConfigChanged: handler("sdkConfigChanged", nil)
case .resyncNeeded: handler("resyncNeeded", nil)
@unknown default: handler("unknown", nil)
}
}
}
// MARK: 데이터 전송
@objc public static func uploadPendingPositions(_ completion: @escaping () -> Void) {
Task { await OneS1ght.uploadPendingPositions(); completion() }
}
@objc public static func discardPendingPositions() {
OneS1ght.discardPendingPositions()
}
// MARK: 변환
private static func dict(_ b: Building) -> [String: Any] {
var d: [String: Any] = ["id": b.id, "name": b.name]
if let count = b.floorCount { d["floorCount"] = count }
return d
}
private static func dict(_ f: Floor) -> [String: Any] {
var d: [String: Any] = ["id": f.id, "name": f.name, "hasPlan": f.hasPlan,
"originX": f.originX, "originY": f.originY,
"widthM": f.widthM, "heightM": f.heightM]
if let image = f.image { d["image"] = image }
return d
}
private static func dict(_ z: Zone) -> [String: Any] {
var d: [String: Any] = ["id": z.id, "name": z.name,
"polygon": z.polygon.map { [$0.x, $0.y] }]
if let dwell = z.dwellSeconds { d["dwellSeconds"] = dwell }
return d
}
private static func dict(_ l: FloorLocators) -> [String: Any] {
var d: [String: Any] = ["locators": l.locators.map {
["address": $0.address, "x": $0.x, "y": $0.y, "z": $0.z] as [String: Any]
}]
if let sessionId = l.sessionId { d["sessionId"] = sessionId }
return d
}
/// Swift 오류 → NSError. domain "OneS1ght", code 3001, userInfo["sdkCode"] = "E3001" 형태
private static func nsError(_ error: Error) -> NSError {
let code: SdkErrorCode
let message: String
switch error {
case let e as SdkError: code = e.code; message = e.code.summary
case let e as ApiError: code = e.code; message = e.description
default: return error as NSError
}
return NSError(domain: "OneS1ght",
code: Int(code.rawValue.dropFirst()) ?? 0,
userInfo: [NSLocalizedDescriptionKey: message,
"sdkCode": code.rawValue])
}
}
제약
- UWB 측위는 포그라운드 전용 — 백그라운드에서 멈추고 돌아오면 측위동작이 재개됩니다.
- 좌표 버퍼는 메모리 — 강제 종료 시 미전송 좌표는 사라집니다.
- 시뮬레이터에서는 측위할 수 없습니다.